pax_global_header00006660000000000000000000000064145232300650014512gustar00rootroot0000000000000052 comment=a8f002bfa8fbcc79bcfe9888be35f20448e593e2 mu-1.10.8/000077500000000000000000000000001452323006500122225ustar00rootroot00000000000000mu-1.10.8/.dir-locals.el000066400000000000000000000021401452323006500146500ustar00rootroot00000000000000;;; -*- no-byte-compile: t; -*- ((nil . ((tab-width . 8) (fill-column . 80) ;; (commment-fill-column . 80) (emacs-lisp-docstring-fill-column . 65) (bug-reference-url-format . "https://github.com/djcb/mu/issues/%s"))) (c-mode . ((c-file-style . "linux") (indent-tabs-mode . t) (mode . bug-reference-prog))) (c-ts-mode . ((indent-tabs-mode . t) (c-ts-mode-indent-style . linux) (c-ts-mode-indent-offset . 8) (mode . bug-reference-prog))) (c++-mode . ((c-file-style . "linux") (fill-column . 100) ;; (comment-fill-column . 80) (mode . bug-reference-prog))) (c++-ts-mode . ((indent-tabs-mode . t) (c-ts-mode-indent-style . linux) (c-ts-mode-indent-offset . 8) (mode . bug-reference-prog))) (emacs-lisp-mode . ((indent-tabs-mode . nil) (mode . bug-reference-prog))) (lisp-data-mode . ((indent-tabs-mode . nil))) (texinfo-mode . ((mode . bug-reference-prog))) (org-mode . ((mode . bug-reference)))) mu-1.10.8/.editorconfig000066400000000000000000000015501452323006500147000ustar00rootroot00000000000000#-*-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". [*.{cc,cpp,hh,hpp}] indent_style = tab indent_size = 8 max_line_length = 90 [*.{c,h}] indent_style = tab indent_size = 8 max_line_length = 80 [configure.ac] indent_style = tab indent_size = 4 max_line_length = 100 [Makefile.am] indent_style = tab indent_size = 8 max_line_length = 100 mu-1.10.8/.github/000077500000000000000000000000001452323006500135625ustar00rootroot00000000000000mu-1.10.8/.github/ISSUE_TEMPLATE/000077500000000000000000000000001452323006500157455ustar00rootroot00000000000000mu-1.10.8/.github/ISSUE_TEMPLATE/feature-request.md000066400000000000000000000011561452323006500214130ustar00rootroot00000000000000--- name: Mu4e Feature request about: Suggest an idea for this project title: "[mu4e rfe]" labels: rfe, mu4e, new assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. mu-1.10.8/.github/ISSUE_TEMPLATE/guile.md000066400000000000000000000010041452323006500173670ustar00rootroot00000000000000--- name: Guile about: mu-guile related item title: "[guile]" labels: new, guile assignees: '' --- **Describe the item** A clear and concise description of what you expected or wished to happen and what actually happened while using mu-guile. **To Reproduce** Steps to reproduce the behavior. **Environment** Please describe the versions of OS, Emacs, mu/mu4e etc. you are using. **Checklist** - [ ] you are running either the latest 1.4.x release, or a 1.5.11+ development release (otherwise, please upgrade). mu-1.10.8/.github/ISSUE_TEMPLATE/misc.md000066400000000000000000000006511452323006500172240ustar00rootroot00000000000000--- name: Misc about: Miscellaneous items you want to share title: "[misc]" labels: new assignees: '' --- **Note**: for questions, please use the mailing-list: https://groups.google.com/g/mu-discuss **Describe the issue** A clear and concise description, i.e. what you expected/desired to happen and what actually happened. **Environment** If applicable, please describe the versions of OS, Emacs, mu etc. you are using. mu-1.10.8/.github/ISSUE_TEMPLATE/mu-bug-report.md000066400000000000000000000011711452323006500207740ustar00rootroot00000000000000--- name: Mu Bug Report about: Create a report to help us improve title: "[mu bug]" labels: bug, mu, new assignees: '' --- **Describe the bug** A clear and concise description of what the bug is, what you expected to happen and what actually happened. **To Reproduce** Detailed steps to reproduce the behavior. If this is about a specific (kind of) message, **always** attach an (anonymized as need) example message. **Environment** Please describe the versions of OS, Emacs, mu etc. you are using. **Checklist** - [ ] you are running either the latest 1.6.x release, or a 1.7.x development release (otherwise, please upgrade). mu-1.10.8/.github/ISSUE_TEMPLATE/mu4e-bug-report.md000066400000000000000000000016571452323006500212360ustar00rootroot00000000000000--- name: Mu4e Bug Report about: Create a report to help us improve title: "[mu4e bug]" labels: bug, mu4e, new assignees: '' --- **Describe the bug** Please provide a clear and concise description of what you expected to happen and what actually happened. **How to Reproduce** Include the exact steps of what you were doing (commands executed etc.). Include any relevant logs and outputs. If this is about a specific (kind of) message, attach an example message. (Open the message, press `.` (`mu4e-view-raw-message`), then `C-x C-w` and attach. Anonymize as needed, all that matters is that the issue still reproduces. **Environment** Please describe the versions of OS, Emacs, mu/mu4e etc. you are using. **Checklist** - [ ] you are running either an 1.8.x release or the latest 1.9.x development release - [ ] you can reproduce the problem without 3rd party extensions (including Doom/Evil etc.) - [ ] you have read all of the above mu-1.10.8/.github/issue_template.md000066400000000000000000000022751452323006500171350ustar00rootroot00000000000000# Important! Before filing an issue, please consider the following: * Ensure your mu/mu4e setup is no older than the latest stable release (1.6.x). * Disable any third-party mu4e extensions; this includes customizations like the ones in "Doom" / "Evil" etc. * If a problem occurs with a certain (type of) message, attach an (anonymized) example of such a message * Please provide some minimal steps to reproduce * Please follow the below template Thanks! ## Expected or desired behavior Please describe the behavior you expect or want ## Actual behavior Please describe the behavior you are actually seeing. For bug-reports, if applicable, include error messages, emacs stack traces, example messages 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 detail as possible how one can reproduce the problem. If there's a problem with a specific (type of) message, please attach such a message to the report. ## Versions of mu, mu4e/emacs, operating system etc. ## Any other detail E.g. are you using the gnus-based message view? mu-1.10.8/.github/workflows/000077500000000000000000000000001452323006500156175ustar00rootroot00000000000000mu-1.10.8/.github/workflows/build-and-test.yml000066400000000000000000000015701452323006500211610ustar00rootroot00000000000000name: Build & run tests on: - push - pull_request jobs: build: runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: os: - ubuntu-latest - macos-latest steps: - uses: actions/checkout@v2 - if: contains(matrix.os, 'ubuntu') name: ubuntu-deps run: | sudo apt update sudo apt-get install meson ninja-build libglib2.0-dev libxapian-dev libgmime-3.0-dev pkg-config guile-3.0-dev emacs texinfo - if: contains(matrix.os, 'macos') name: macos-deps run: | brew install meson ninja libgpg-error libtool pkg-config glib gmime xapian guile emacs texinfo - name: configure run: ./autogen.sh '-Db_sanitize=address' - name: build run: make - name: test run: make test-verbose-if-fail mu-1.10.8/.gitignore000066400000000000000000000032001452323006500142050ustar00rootroot00000000000000www/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-config.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 /lib/utils/test-sexp /lib/utils/test-option /lib/test-mu-threader /lib/test-mu-tokenizer /lib/test-mu-parser /lib/test-mu-query-threader /lib/test-contacts /lib/test-flags /lib/test-maildir /lib/test-msg /lib/test-msg-fields /lib/test-query /lib/test-store /lib/test-threader /mu/test-cmd /mu/test-cmd-cfind /mu/test-query /mu/test-threads /lib/test-threads mu-1.10.8/.mailmap000066400000000000000000000000531452323006500136410ustar00rootroot00000000000000Dirk-Jan C. Binnema mu-1.10.8/AUTHORS000066400000000000000000000000531452323006500132700ustar00rootroot00000000000000Dirk-Jan C. Binnema mu-1.10.8/COPYING000066400000000000000000001045131452323006500132610ustar00rootroot00000000000000 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 . mu-1.10.8/ChangeLog000066400000000000000000000000151452323006500137700ustar00rootroot00000000000000See NEWS.org mu-1.10.8/Makefile000066400000000000000000000074111452323006500136650ustar00rootroot00000000000000## Copyright (C) 2008-2023 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. # Makefile with some useful targets for meson/ninja NINJA ?= ninja BUILDDIR ?= $(CURDIR)/build COVERAGE_BUILDDIR ?= $(CURDIR)/build-coverage MESON ?= meson V ?= 0 ifneq ($(V),0) VERBOSE=--verbose endif # when MU_HACKER is set, do a debug build # MU_HACKER is for djcb & compatible developers ifneq (${MU_HACKER},) MESON_FLAGS:=$(MESON_FLAGS) '-Dbuildtype=debug' \ '-Db_sanitize=address' \ '-Dreadline=enabled' endif .PHONY: all .PHONY: check test test-verbose-if-fail test-valgrind test-helgrind .PHONY: benchmark coverage .PHONY: dist install uninstall clean distclean .PHONY: mu4e-doc-html # MESON_FLAGS, e.g. "-Dreadline=enabled" # examples: # 1. build with clang, and the thread-sanitizer # make clean all MESON_FLAGS="-Db_sanitize=thread" CXX=clang++ CC=clang all: $(BUILDDIR) @$(NINJA) -C $(BUILDDIR) $(VERBOSE) @ln -sf $(BUILDDIR)/compile_commands.json $(CURDIR) || /bin/true $(BUILDDIR): @$(MESON) setup $(MESON_FLAGS) $(BUILDDIR) check: test test: all @$(MESON) test $(VERBOSE) -C $(BUILDDIR) install: $(BUILDDIR) @cd $(BUILDDIR); $(MESON) install uninstall: $(BUILDDIR) @$(NINJA) -C $(BUILDDIR) uninstall clean: @rm -rf $(BUILDDIR) $(COVERAGE_BUILDDIR) # # below targets are just for development/testing/debugging. They may or # may not work on your system. # test-verbose-if-fail: all @cd $(BUILDDIR); $(MESON) test || $(MESON) test --verbose vg_opts:=--enable-debuginfod=no --leak-check=full --error-exitcode=1 test-valgrind: export G_SLICE=always-malloc test-valgrind: export G_DEBUG=gc-friendly test-valgrind: $(BUILDDIR) @cd $(BUILDDIR); $(MESON) test \ --wrap="valgrind $(vg_opts)" \ --timeout-multiplier 100 # we do _not_ pass helgrind; but this seems to be a false-alarm # https://gitlab.gnome.org/GNOME/glib/-/issues/2662 # test-helgrind: $(BUILDDIR) # @cd $(BUILDDIR); TEST=HELGRIND $(MESON) test \ # --wrap='valgrind --tool=helgrind --error-exitcode=1' \ # --timeout-multiplier 100 benchmark: $(BUILDDIR) $(NINJA) -C $(BUILDDIR) benchmark $(COVERAGE_BUILDDIR): $(MESON) -Db_coverage=true --buildtype=debug $(COVERAGE_BUILDDIR) covfile:=$(COVERAGE_BUILDDIR)/meson-logs/coverage.info # generate by hand, meson's built-ins are unflexible coverage: $(COVERAGE_BUILDDIR) $(NINJA) -C $(COVERAGE_BUILDDIR) test lcov --capture --directory . --output-file $(covfile) @lcov --remove $(covfile) '/usr/*' '*guile*' '*thirdparty*' '*/tests/*' '*mime-object*' --output $(covfile) @lcov --remove $(covfile) '*mu/mu/*' --output $(covfile) @mkdir -p $(COVERAGE_BUILDDIR)/meson-logs/coverage @genhtml $(covfile) --output-directory $(COVERAGE_BUILDDIR)/meson-logs/coverage/ @echo "coverage report at: file://$(COVERAGE_BUILDDIR)/meson-logs/coverage/index.html" dist: $(BUILDDIR) @cd $(BUILDDIR); $(MESON) dist distclean: clean HTMLPATH=${BUILDDIR}/mu4e/mu4e mu4e-doc-html: @mkdir -p ${HTMLPATH} && cp mu4e/texinfo-klare.css ${HTMLPATH} @makeinfo -I ${BUILDDIR}/mu4e --html --css-ref=texinfo-klare.css -o ${HTMLPATH} mu4e/mu4e.texi mu-1.10.8/NEWS000066400000000000000000000000161452323006500127160ustar00rootroot00000000000000See NEWS.org mu-1.10.8/NEWS.org000066400000000000000000001636061452323006500135230ustar00rootroot00000000000000#+STARTUP:showall * NEWS (user visible changes & bigger non-visible ones) * 1.10 (releases on March 26, 2023) *** mu - a new command-line parser, which allows (hopefully!) for a better user interaction; better error checking and more - Invalid e-mail addresses are no longer added to the contacts-cache. - The ~cfind~ command gained ~--format=json~, which makes it easy to further process contact information, e.g. using ~jq~. See the manpage for more details. - The ~init~ command learned ~--reinit~ to reinitialize the database with the setings of an exisitng one - The ~script~ command is gone, and integrated with ~mu~ directly, i.e. the scripts (when enabled) are directly visible in the ~mu~ output. Also see the Guile section. - The ~extract~ command gained the ~--uncooked~ option to tell it to _not_ replace spaces with dashes in extracted filenames (and a few other things). - Revamped manpages which are now generated from ~org~ descriptions - Standardize on PCRE-flavored regular expressions throughout *mu*. - ~mu~ no longer attempts to 'expand' the `~` (and some other characters) in command line options that take filenames, since it was a bit unpredictable. So write e.g. ~--option=/home/user/hello~ instead of ~--option=~/hello~ - Experimental: as bit of a hack, html message bodies are processed as if they were plain text, similar how "old mu" would do it (1.6.x and earlier). A nicer solution would be to convert to text, but this something for the future. - the MSYS2 (Windows) builds is _experimental_ now; some things may not work; see e.g. https://github.com/djcb/mu/issues?q=is%3Aissue+label%3Amsys, but we welcome efforts to fix those things. *** mu4e - ~emacs~ 26.3 or higher is now required for ~mu4e~ - ~mu4e-view-mode-hook~ now fires before the message is rendered. If you have hook-functions that depend on the message contents, you should use the new ~mu4e-view-rendered-hook~. - mu4e window management has been completely reworked and cleaned up, affecting the message loading as well as the window-layout. As a user-visible feature, there's now the =z= binding (~mu4e-view-detach~), to 'detach' view and alllow for keeping multiple views around; see the [[info:mu4e:MSGV Detaching and reattaching][manual entry]] for further details. - As a result of that, ~mu4e-split-view~ can no longer be a function; the new way is to use ~display-buffer-alist~ as explained in the [[info:mu4e:Buffer Display][manual]] - ~mu4e~ now keeps track of 'baseline' query results and shows the difference from that in the main view and modeline (you'll might see something like =1(+1)/2= for your bookmarks or in the modeline; that means that there is one more unread message since baseline; see the [[info:mu4e#Bookmarks and Maildirs][manual entry]] for details. The idea is that you get a quick overview of where changes happened while you were doing something else. This is a somewhat experimental feature which is under active development - Related to that, you can now crown one of your bookmarks in =mu4e-bookmarks= with ~:favorite t~, causing it to be highlighted in the main view and used in the mode-line. See the new [[info:mu4e#Modeline][modeline entry]] in the manual; this uses the new =mu4e-modeline-mode= minor-mode. - Expanding on that further, you can also get desktop notifications for new mail (on systems with DBus for now; see [[info:mu4e:#Desktop notifications][Desktop notifications]] in the manual. - If your search query matches some bookmark, the modeline now shows the bookmark's name rather than the query; this can be controlled through =mu4e-modeline-prefer-bookmark-name= (default: =t=). - You can now tell mu4e to use emacs' completion system rather than the mu4e built-in one; see the variables ~mu4e-read-option-use-builtin~ and ~mu4e-completing-read-function~; e.g. to always emacs completion (which may have been enhanced by various completion frameworks), use: #+begin_src elisp (setq mu4e-read-option-use-builtin nil mu4e-completing-read-function 'completing-read) #+end_src - when moving messages (which includes changing flags), file-flags changes are propagated to duplicates of the messages; that is, e.g. the /Seen/ or /Replied/ status is propagated to all duplicates (earlier, this was only done when marking a message as read). Note, /Draft/, /Flagged/ and /Trashed/ flags are deliberately *not* propagated. - Teach ~mu4e-copy-thing-at-point~ about ~shr~ links - The ~mu4e-headers-toggle-setting~ has been renamed ~mu4e-headers-toggle-property~ and has the new default binding ~P~, which works in both the headers-view and message-view. The older functions ~mu4e-headers-toggle-threading~, ~mu4e-headers-toggle-threading~, ~mu4e-headers-toggle-full-search~ ~mu4e-headers-toggle-include-related~, ~full-search~skip-duplicates~ have been removed (with their keybindings) in favor of ~mu4e-headers-toggle-property~. - There's also a new property ~mu4e-headers-hide-enabled~, which controls wheter ~mu4e-headers-hide-predicate~ is applied (when non-~nil~). This can be used to temporarily turn the predicate off/on. - You can now jump to previous / next threads in headers-view, message view. Default binding is ~{~ and ~}~, respectively. - When searching, the number of hidden messages is now shown in the message footer along with the number of Found messages - The ~eldoc~ support in header-mode is now optional and disabled by default; set ~mu4e-eldoc-support~ to non-nil to enable it. - In the main view, the keybindings shown are a representation of the actual keybindings, rather than just the defaults. This is for the benefit for people who want to use different keybindings. - As a side-effect of that, ~mu4e-main-mode~ and ~mu4e-main-mode-hook~ functions are now invoked _before_ the rendering takes place; if you're customizations depend on happening after rendering is completed, use the new ~mu4e-main-rendered-hook~ instead. - ~mu4e-cache-maildir-list~ has been promoted to be a =defcustom=, enabled by default. This caches the list of "other" maildirs (i.e., without a shortcut). - For testing, a new command ~mu4e-server-repl~ to start a ~mu~ server just as ~mu4e~ does it. Note that this cannot run at the same time when ~mu4e~ runs. - all the obsolete function and variable aliases have been moved to ~mu4e-obsolete.el~ so we can unclutter the non-obsolete code a bit. *** guile - in the 1.8 release, the /current/ Guile API was deprecated; that does not mean that Guile support goes way, just that it will look different. - Guile script commands are now integrated with the main ~mu~, so without further parameters ~mu~ shows both subcommands and scripts. This is a work-in-progress! - The per-(week|day|year|year-month) scripts have been combined into a ~histogram~ script. If you have Guile-support enabled, and have ~gnuplot~ installed, you can do e.g., #+begin_example mu histogram -- --time-unit=day --query="hello" #+end_example to get a histogram of such messages. Note, this area is under active developement and will likely change. *** building and installation - the autotools build (which was deprecated since 1.8) has now been removed. we thank it for its services since 2008. We continue with ~meson~. However, we still have ~autogen.sh~ and a ~Makefile~ which can be helpful for driving ~meson~-based builds. Think of the ~Makefile~ as a convenient place to put common action for which I always forget the ~meson~ incantation.** - ~meson~ 56.0 or higher is required for building - ~emacs~ 26.3 or higher is needed for ~mu4e~ *** internals As usual, there have been a number of internal updates in the ~mu~ codebase: - reworked the internal s-expression parser - new command-line argument parser (based on CLI11) - message-move flag propagation moved from the mu4e-server to mu-store - more =mu4e~= internals have been renamed/reworked in to ~mu4e--~. *** contributor to this release Aimé Bertrand, Aleksei Atavin, Al Haji-Ali, Andreas Hindborg, Anton Tetov, Arsen Arsenović, Babak Farrokhi, Ben Cohen, Damon Kwok, Daniel Colascione, Derek Zhou, Dirk-Jan C. Binnema, John Hamelink, Leo Gaskin, Manuel Wiesinger, Marcel van der Boom, Mark Knoop, Mickey Petersen, Nicholas Vollmer, Protesilaos Stavrou, Remco van 't Veer, Sean Allred, Sean Farley, Stephen Eglen, Tassilo Horn And of course all the people how filed tickets, asked question, provided suggestions. * Old news :PROPERTIES: :VISIBILITY: folded :END: ** 1.8 (released on June 25, 2022) (there are some changes in the installation procedure compared to 1.6.x; see Installation below) **** mu - The server protocol (as used my mu4e) has seen a number of updates, to allow for faster rendering. As before, there's no compatibility between minor release numbers (1.4 vs 1.6 vs 1.8) nor within development series (such as 1.7). However, within a stable release (such as all 1.6.x) the protocol won't change (except if required to fix some severe bug; this never happened in practice) - The ~processed~ number in the indexing statistics has been renamed into ~checked~ and describes the number of message files considered for updating, which is a bit more useful that the old value, which was more-or-less synonymous with the ~updated~ number (which are the messages that got (re)parsed / (re)added to the database. Basically, it counts all the messages for which we checked their timestamp. - The internals of the message handling in ~mu~ have been heavily reworked; much of this is not immediately visible but is an enabler for some new features. - instead of passing ~--muhome~, you can now also set an environment variable ~MUHOME~. - the ~info~ command now includes information about the last indexing operation and the last database change that took place; note that the information may be slightly delayed due to database caching. - the ~verify~ command for checking signatures has been updated, and is more informative - a new command ~fields~ provides information about the message fields and flags for use in queries. The information is the same information that ~mu~ uses and so stays up to date. - a new message field ~changed~, which refers to the time/date of the last time a message was changed (the file ~ctime~) - new message flags ~personal~ to search for "personal" messages, which are defined as a message with at least one personal contact, and ~calendar~ for messages with calendar-invitations. - message sexps are now cached in the store, which makes delivering sexp-based search results (as used by ~mu4e~) much faster. - Windows/MSYS support is deprecated; it doesn't work well (if at all) and there's currently not sufficient developer interest/expertise to change this. **** mu4e - the old mu4e-view is *gone*; only the gnus-based one remains. This allowed for removing quite a bit of old code. - the mu4e headers rendering is much faster (a factor of 3+), which makes displaying big results snappier. This required some updates in the headers handling and in the server protocol. Separate from that, the cached message sexps (see the ~mu~ section) make getting the results much faster. This becomes esp. clear when there are a lot of query results. - "related" messages are now recognizable as such in the headers-view, with their own face, ~mu4e-related-face~; by default with an italic slant. - For performance testing, you can set the variable ~mu4e-headers-report-render-time~ to ~t~ and ~mu4e~ will report the search/rendering speed of each query operation. - Removed header-fields ~:attachments~, ~:signature~, ~:encryption~ and ~:user-agent~. They're obsolete with the Gnus-based message viewer. - The various "toggles" for the headers-view (full-search, include-related, skip-duplicates, threading) were a bit hard to find and with non-obvious key-bindings. For that, there is now ~mu4e-headers-toggle-setting~ (bound to ~M~) to handle all of that. The toggles are also reflected in the mode-line; so e.g. 'RTU' means we're including [R]elated messages, and show [T]hreads, skip duplicates ([U]nique). - A new ~defcustom~, ~mu4e-view-open-program~ for starting the appropriate program for a give file (e.g., ~xdg-open~). There are some reasonable defaults for various systems. This can also be set to a function. - indexing happens in the background now and mu4e can interact with the server while it is ongoing; this allows for using mu4e during lengthy indexing operations. - ~mu4e-index-updated-hook~ now fires after indexing completed, regardless of whether anything changed (before, it fired only if something changed). In your hook-functions (or elsewhere) you can check if anything changed using the new variable ~mu4e-index-update-status~. And note that ~processed~ has been renamed into ~checked~, with a slightly different meaning, see the mu section. - ~message-user-organization~ can now be used to set the ~Organization:~ header. See its docstring for details. - ~mu4e-compose-context-switch~ no longer attempts to update the draft folder (which turned out to be a little fragile). However, it has been updated to automatically change the ~Organization:~ header, and attempts to update the message signature. Also, there's a key-binding now: ~C-c ;~ - Changed the default for ~mu4e-compose-complete-only-after~ to 2018-01-01, to filter out contacts not seen after that date. - As an additional measure to limit the number of contacts that mu4e loads for auto-completions, there's ~mu4e-compose-complete-max~, to set a precise numerical match (*before* any possible filtering). Set to ~nil~ (no maximum by default). - Updated the "fancy" characters for some header fields. Added new ones for personal and list messages. - Removed ~make-mu4e-bookmark~ which was obsoleted in version 1.3.9. - Add command ~mu4e-sexp-at-point~ for showing/hiding the s-expression for the message-at-point. Useful for development / debugging. Bound to ~,~ in headers and view mode. - undo is now supported across message-saves - a lot of the internals have been changed: - =mu4e= is slowly moving from using the '=~'= to the more common '=--'= separator for private functions; i.e., =mu4e-foo= becomes =mu4e--foo=. - =mu4e-utils.el= had become a bit of a dumping ground for bits of code; it's gone now, with the functionality move to topic-specific files -- =mu4e-folders.el=, =mu4e-bookmarks.el=, =mu4e-update.el=, and included in existing files. - the remaining common functionality has ended up in =mu4e-helpers.el= - =mu4e-search.el= takes the search-specific code from =mu4e-headers.el=, and adds a minor-mode for the keybindings. - =mu4e-context.el= and =mu4e-update.el= also define minor modes with keybindings, which saves a lot of code in the various views, since they don't need explicitly bind all those function. - also =mu4e-vars.el= had become very big, we're refactoring the =defvar= / =defcustom= declarations to the topic-specific files. - =mu4e-proc.el= has been renamed =mu4e-server.el=. - Between =mu= and =mu4e=, contact cells are now represented as a plist ~(:name "Foo Bar" :email "foobar@example.com")~ rather than a cons-cell ~("Foo Bar" . "foobar@example.com").~ If you have scripts depending on the old format, there's the ~mu4e-contact-cons~ function which takes a news-style contact and yields the old form. - Because of all these changes, it is recommended you remove older version of ~mu4e~ before reinstalling. **** guile - the current guile support has been deprecated. It may be revamped at some point, but will be different from the current one, which is to be removed after 1.8 **** toys - the ~toys~ (~mug~) has been removed, as they no longer worked with the rest of the code. *** Installation - =mu= switched to the [[https://mesonbuild.com][meson]] build system by default. The existing =autotools= is still available, but is to be removed after the 1.8 release. Using =meson= (which you may need to install), you can use something like the following in the mu top source directory: #+BEGIN_SRC sh $ meson build && ninja -C build #+END_SRC - However, note that =autogen.sh= has been updated, and there's a convenience =Makefile= with some useful targets, so you can also do: #+BEGIN_SRC sh $ ./autogen.sh && make # and optionally, 'sudo make install' #+END_SRC - After that, either =ninja -C build= or =make= should be enough to rebuild - NOTE: development versions 1.7.18 - 17.7.25 had a bug where the mail file names sometimes got misnamed (with some extra ':2,'). This can be restored with something like: #+begin_example $ find ~/Maildir -name '*:2,*:*' | \ sed "s/\(\([^:]*\)\(:2,\)\{1,\}\(:2,.*$\)\)/mv '\0' '\2\4'/" > rename.sh #+end_example (replace 'Maildir' with the path to your maildir) once this is done, do check the generated 'rename.sh' and after convincing yourself it does the right thing, do #+begin_example $ sh rename.sh #+end_example after that, re-index. - Before installing, it is recommended that you *remove* any older versions of ~mu~ and especially ~mu4e~, since they may conflict with the newer ones. - =mu= now requires C++17 support for building *** Contributor for this release - As per ~git~: c0dev0id, Christophe Troestler, Daniel Fleischer, Daniel Nagy, Dirk-Jan C. Binnema, Dr. Rich Cordero, Kai von Fintel, Marcelo Henrique Cerri, Nicholas Vollmer, PRESFIL, Tassilo Horn, Thierry Volpiatto, Yaman Qalieh, Yuri D'Elia, Zero King - And of course all the people filing issues, suggesting features and helping out on the maling list. ** 1.6 (released, as of July 27 2021) NOTE: After upgrading, you need to call ~mu init~, with your prefered parameters before you can use ~mu~ / ~mu4e~. This is because the underlying database-schema has changed. *** mu - Where available (and with suitably equiped ~libglib~), log to the ~systemd~ journal instead of =~/.cache/mu.log=. Passing the ~--debug~ option to ~mu~ increases the amount that is logged. - Follow symlinks in maildirs, and support moving messsages across filesystems. Obviously, that is typically quite a bit slower than the single-filesystem case, but can be still be useful. - Optionally provide readline support for the ~mu~ server (when in tty-mode) - Reworked the way mu generates s-expressions for mu4e; they are created programmatically now instead of through string building. - The indexer (the part of mu that scans maildirs and updates the message store) has been rewritten so it can work asynchronously and take advantage of multiple cores. Note that for now, indexing in ~mu4e~ is still a blocking operation. - Portability updates for dealing with non-POSIX systems, and in particular VFAT filesystem, and building using Clang/libc++. - The personal addresses (as per ~--my-address=~ for ~mu init~) can now also include regular expressions (basic POSIX); wrap the expression in ~/~, e.g., ~--my-address='/.*@example.*/~'. - Modernized the querying/threading machinery; this makes some old code a lot easier to understand and maintain, and even while not an explicit goal, is also faster. - Experimental support for the Meson build system. *** mu4e - Use the gnus-based message viewer as the default; the new viewer has quite a few extra features compared to the old, mu4e-specific one, such as faster crypto, support for S/MIME, syntax-highlighting, calendar invitations and more. The new view is superior in most ways, but if you still depend on something from the old one, you can use: #+begin_example ;; set *before* loading mu4e; and restart emacs if you want to change it ;; users of use-packag~ should can use the :init section for this. (setq mu4e-view-use-old t) #+end_example (The older variable ~mu4e-view-use-gnus~ with the opposite meaning is obsolete now, and no longer in use). - Include maildir-shortcuts in the main-view with overall/unread counts, similar to bookmarks, and with the same ~:hide~ and ~:hide-unread~ properties. Note that for the latter, you need to update your maildir-shortcuts to the new format, as explained in the ~mu4e-maildir-shortcuts~ docstring. You can set ~mu4e-main-hide-fully-read~ to hide any bookmarks/maildirs that have no unread messages. - Add some more properties for use in capturing org-mode links to messages / queries. See [[info:mu4e#Org-mode links][the mu4e manual]] for details. - Honor ~truncate-string-ellipsis~ so you can now use 'fancy' ellipses for truncated strings with ~(setq truncate-string-ellipsis "…")~ - Add a variable ~mu4e-mu-debug~ which, when set to non-~nil,~ makes the ~mu~ server log more verbosely (to ~mu.log~ or the journal) - Better alignment in headers-buffers; this looks nicer, but is also a bit slower, hence you need to enable ~mu4e-headers-precise-alignment~ for this. - Support ~mu~'s new regexp-based personal addresses, and add ~mu4e-personal-address-p~ to check whether a given string matches a personal address. - TAB-Completion for writing ~mu~ queries - Switch the context for existing draft messages using ~mu4e-compose-context-switch~ or ~C-c C-;~ in ~mu4e-compose-mode~. ** 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. *** guile - guile 3.0 is now supported; guile 2.2 still works. *** 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 #+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 ** 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 06:32 Nu To Edmund Dantès GstDev + Re: Gstreamer-V4L... 15:08 Nu Abbé Busoni GstDev + 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 \ 2013-03-18 S Beachamp EmacsUsr + Re: Copying a whole... 22:07 Nu Albert de Moncerf EmacsUsr \ 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 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.10 *** 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: mu-1.10.8/README.org000066400000000000000000000113671452323006500137000ustar00rootroot00000000000000#+TITLE:mu [[https://github.com/djcb/mu/blob/master/COPYING][https://img.shields.io/github/license/djcb/mu?logo=gnu&.svg]] [[https://en.cppreference.com][https://img.shields.io/badge/Made%20with-C/CPP-1f425f?logo=c&.svg]] [[https://img.shields.io/github/v/release/djcb/mu][https://img.shields.io/github/v/release/djcb/mu.svg]] [[https://github.com/djcb/mu/graphs/contributors][https://img.shields.io/github/contributors/djcb/mu.svg]] [[https://github.com/djcb/mu/issues][https://img.shields.io/github/issues/djcb/mu.svg]] [[https://github.com/djcb/mu/issues?q=is%3Aissue+is%3Aopen+label%3Arfe][https://img.shields.io/github/issues/djcb/mu/rfe?color=008b8b.svg]] [[https://github.com/djcb/mu/pull/new][https://img.shields.io/badge/PRs-welcome-brightgreen.svg]]\\ [[https://www.gnu.org/software/emacs/][https://img.shields.io/badge/Emacs-26.3-922793?logo=gnu-emacs&logoColor=b39ddb&.svg]] [[https://www.djcbsoftware.nl/code/mu/mu4e/Installation.html#Dependencies-for-Debian_002fUbuntu][https://img.shields.io/badge/Platform-Linux-2e8b57?logo=linux&.svg]] [[https://www.djcbsoftware.nl/code/mu/mu4e/Installation.html#Building-from-a-release-tarball-1][https://img.shields.io/badge/Platform-FreeBSD-8b3a3a?logo=freebsd&logoColor=c32136&.svg]] [[https://formulae.brew.sh/formula/mu#default][https://img.shields.io/badge/Platform-macOS-101010?logo=apple&logoColor=ffffff&.svg]] [[https://github.com/msys2-unofficial/MSYS2-packages/blob/master/mu/README.org][https://img.shields.io/badge/Platform-Windows-00bfff?logo=windows&logoColor=00bfff&.svg]] Welcome to ~mu~! *Note*: you are looking at the 1.10 *stable branch*, which is meant for common users and distributions - pick up one of the [[https://github.com/djcb/mu/releases][1.10 Releases]]. If you are interested in ~mu~ development, you can check the [[https://github.com/djcb/mu][master branch]] where that happens. 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. =mu= is fully documented. After indexing your messages into a [[http://www.xapian.org][Xapian]]-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 3.0 and later) ~mu~ is written in 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. All of the code is distributed under the terms of the [[https://www.gnu.org/licenses/gpl-3.0.en.html][GNU General Public License version 3]] (or higher). * Installation Note: building from source is an /advanced/ subject; esp. if something goes wrong. The below simple examples are a start, but all tools involved have many options; there are differences between systems, versions etc. So if this is all a bit daunting we recommend to wait for someone else to build it for you, such as a Linux distribution. Many have packages available. ** Requirements To be able to build ~mu~, ensure you have: - a C++17 compiler (~gcc~ or ~clang~ are known to work) - development packages for /Xapian/ and /GMime/ and /GLib/ (see ~meson.build~ for the versions) - basic tools such as ~make~, ~sed~, ~grep~ - ~meson~ For ~mu4e~, you also need ~emacs~. Note, the MSYS2 support for Windows is _experimental_, that is, it works for some people, but we can't really support it due to lack of the specific expertise. Help is welcome! ** Building #+begin_example $ git clone git://github.com/djcb/mu.git $ cd mu #+end_example ~mu~ uses ~meson~ for building, so you can use that directly, and all the usual commands apply. You can also use it _indirectly_ through the provided ~Makefile~, which provides a number of useful targets. For instance, using the ~Makefile~, you could install ~mu~ using: #+begin_example $ ./autogen.sh && make $ sudo make install #+end_example Alternatively, you can run ~meson~ directly: #+begin_example $ meson build && ninja -C build $ ninja -C build install #+end_example This allows for passing various ~meson~ options, such as ~--prefix~. Consult the ~meson~ documentation for details. mu-1.10.8/autogen.sh000077500000000000000000000011361452323006500142240ustar00rootroot00000000000000#!/bin/sh # Run this to generate all the initial makefiles, etc. echo "*** meson build setup" test -f mu/mu.cc || { echo "*** Run this script from the top-level mu source directory" exit 1 } BUILDDIR=build command -v meson 2> /dev/null if [ $? != 0 ]; then echo "*** No meson found, please install it ***" exit 1 fi # we could remove build/ but let's avoid rm -rf risks... if test -d ${BUILDDIR}; then meson --reconfigure ${BUILDDIR} $@ else meson ${BUILDDIR} $@ fi echo "*** Now run 'ninja -C ${BUILDDIR}' to build mu" echo "*** Or check the Makefile for some useful targets" mu-1.10.8/build-aux/000077500000000000000000000000001452323006500141145ustar00rootroot00000000000000mu-1.10.8/build-aux/meson-install-info.sh000066400000000000000000000004671452323006500201750ustar00rootroot00000000000000#!/bin/sh infodir=$1 infofile=$2 # Meson post-install script to update info metadata # If DESTDIR is set, do _not_ install-info, since it's only a temporary # install if test -z "${DESTDIR}"; then install-info --info-dir "${infodir}" "${infodir}/${infofile}" gzip --force "${infodir}/${infofile}" fi mu-1.10.8/build-aux/version.texi.in000066400000000000000000000001471452323006500171030ustar00rootroot00000000000000@set UPDATED @UPDATED@ @set UPDATED-MONTH @UPDATEDMONTH@ @set EDITION @VERSION@ @set VERSION @VERSION@ mu-1.10.8/contrib/000077500000000000000000000000001452323006500136625ustar00rootroot00000000000000mu-1.10.8/contrib/mu-completion.zsh000066400000000000000000000063641452323006500172110ustar00rootroot00000000000000#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: mu-1.10.8/contrib/mu-sexp-convert000077500000000000000000000144221452323006500166670ustar00rootroot00000000000000#!/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: mu-1.10.8/contrib/mu.spec000066400000000000000000000066251452323006500151700ustar00rootroot00000000000000 # 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. mu-1.10.8/guile/000077500000000000000000000000001452323006500133275ustar00rootroot00000000000000mu-1.10.8/guile/compile-scm.in000066400000000000000000000015611452323006500160720ustar00rootroot00000000000000#!/bin/sh ## Copyright (C) 2021 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. @abs_builddir@/build-env @guild@ compile "$@" # Local-Variables: # mode: sh # End: mu-1.10.8/guile/examples/000077500000000000000000000000001452323006500151455ustar00rootroot00000000000000mu-1.10.8/guile/examples/contacts-export000077500000000000000000000052231452323006500202320ustar00rootroot00000000000000#!/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: mu-1.10.8/guile/examples/msg-graphs000077500000000000000000000110601452323006500171410ustar00rootroot00000000000000#!/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: mu-1.10.8/guile/examples/mu-biff000077500000000000000000000035531452323006500164260ustar00rootroot00000000000000#!/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: mu-1.10.8/guile/examples/org2mu4e000077500000000000000000000050041452323006500165360ustar00rootroot00000000000000#!/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: mu-1.10.8/guile/fdl.texi000066400000000000000000000510301452323006500147660ustar00rootroot00000000000000@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: mu-1.10.8/guile/meson.build000066400000000000000000000121771452323006500155010ustar00rootroot00000000000000## Copyright (C) 2022 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. # # create a shell script for compiling from the source dirs compile_scm_conf = configuration_data() compile_scm_conf.set('abs_builddir', meson.current_build_dir()) compile_scm_conf.set('guild', 'guild') compile_scm=configure_file( input: 'compile-scm.in', output: 'compile-scm', configuration: compile_scm_conf, install: false ) run_command('chmod', '+x', compile_scm, check: true) scm_compiler=join_paths(meson.current_build_dir(), 'compile-scm') # # NOTE: snarfing works but you get: # ,---- # | cc1plus: warning: command-line option ‘-std=gnu11’ is valid for C/ObjC # | but not for C++ # `---- # this is because the snarf-script hardcodes the '-std=gnu11' but we're # building for c++; even worse, e.g. on some MacOS, the warning is a # hard error. # # We can override flag through a env variable CPP; but then we _also_ need to # override the compiler, so e.g. CPP="g++ -std=c++17'; but it's a bit # hairy/ugly/fragile to derive the raw compiler name in meson; also the # generator expression doesn't take an 'env:' parameter, so we'd need # to rewrite using custom_target... # # for now, we avoid all that by simply including the generated files. do_snarf=false if do_snarf snarf = find_program('guile-snarf3.0','guile-snarf') # there must be a better way of feeding the include paths to snarf... snarf_args=['-o', '@OUTPUT@', '@INPUT@', '-I' + meson.current_source_dir() + '/..', '-I' + meson.current_source_dir() + '/../lib', '-I' + meson.current_build_dir() + '/..'] snarf_args += '-I' + join_paths(glib_dep.get_pkgconfig_variable('includedir'), 'glib-2.0') snarf_args += '-I' + join_paths(glib_dep.get_pkgconfig_variable('libdir'), 'glib-2.0', 'include') snarf_args += '-I' + join_paths(guile_dep.get_pkgconfig_variable('includedir'), 'guile', '3.0') snarf_gen=generator(snarf, output: '@BASENAME@.x', arguments: snarf_args) snarf_srcs=['mu-guile.cc', 'mu-guile-message.cc'] snarf_x=snarf_gen.process(snarf_srcs) else snarf_x = [ 'mu-guile-message.x', 'mu-guile.x' ] endif lib_guile_mu = shared_module( 'guile-mu', [ 'mu-guile.cc', 'mu-guile-message.cc' ], dependencies: [guile_dep, glib_dep, lib_mu_dep, config_h_dep, thread_dep ], install: true, install_dir: guile_extension_dir ) if makeinfo.found() custom_target('mu_guile_info', input: 'mu-guile.texi', output: 'mu-guile.info', install: true, install_dir: infodir, command: [makeinfo, '-o', join_paths(meson.current_build_dir(), 'mu-guile.info'), join_paths(meson.current_source_dir(), 'mu-guile.texi'), '-I', join_paths(meson.current_build_dir(), '..')]) if install_info.found() infodir = join_paths(get_option('prefix') / get_option('infodir')) meson.add_install_script(install_info_script, infodir, 'mu-guile.info') endif endif guile_scm_dir=join_paths(datadir, 'guile', 'site', '3.0') install_data(['mu.scm'], install_dir: guile_scm_dir) guile_scm_mu_dir=join_paths(guile_scm_dir, 'mu') foreach mod : ['script.scm', 'message.scm', 'stats.scm', 'plot.scm'] install_data(join_paths('mu', mod), install_dir: guile_scm_mu_dir) endforeach mu_guile_scripts=[ join_paths('scripts', 'find-dups.scm'), join_paths('scripts', 'msgs-count.scm'), join_paths('scripts', 'histogram.scm')] mu_guile_script_dir=join_paths(datadir, 'mu', 'scripts') install_data(mu_guile_scripts, install_dir: mu_guile_script_dir) guile_builddir=meson.current_build_dir() # # guile test; does't work wit asan. # if get_option('b_sanitize') == 'none' guile_load_path=':'.join([ join_paths(meson.project_source_root(), 'guile'), meson.current_build_dir()]) test('test-mu-guile', executable('test-mu-guile', join_paths('tests', 'test-mu-guile.cc'), install: false, cpp_args: [ '-DABS_SRCDIR="' + join_paths(meson.current_source_dir(), 'tests') + '"', '-DGUILE_LOAD_PATH="' + guile_load_path + '"', '-DGUILE_EXTENSIONS_PATH="' + guile_load_path + '"' ], dependencies: [glib_dep, lib_mu_dep])) else message('sanitizer build; skip guile test') endif mu-1.10.8/guile/mu-guile-message.cc000066400000000000000000000306601452323006500170110ustar00rootroot00000000000000/* ** Copyright (C) 2011-2021 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-guile-message.hh" #include #include "message/mu-message.hh" #include "utils/mu-utils.hh" #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wredundant-decls" #include #pragma GCC diagnostic pop #include "mu-guile.hh" #include #include using namespace Mu; /* pseudo field, not in Xapian */ constexpr auto MU_GUILE_MSG_FIELD_ID_TIMESTAMP = Field::id_size() + 1; /* some symbols */ static SCM SYMB_PRIO_LOW, SYMB_PRIO_NORMAL, SYMB_PRIO_HIGH; static std::array SYMB_FLAGS; static SCM SYMB_CONTACT_TO, SYMB_CONTACT_CC, SYMB_CONTACT_BCC, SYMB_CONTACT_FROM; static long MSG_TAG; using MessageSPtr = std::unique_ptr; static gboolean mu_guile_scm_is_msg(SCM scm) { return SCM_NIMP(scm) && (long)SCM_CAR(scm) == MSG_TAG; } static SCM message_scm_create(Xapian::Document&& doc) { /* placement-new */ void *scm_mem{scm_gc_malloc(sizeof(Message), "msg")}; Message* msgp = new(scm_mem)Message(std::move(doc)); SCM_RETURN_NEWSMOB(MSG_TAG, msgp); } static const Message* message_from_scm(SCM msg_smob) { return reinterpret_cast(SCM_CDR(msg_smob)); } static size_t message_scm_free(SCM msg_smob) { if (auto msg = message_from_scm(msg_smob); msg) msg->~Message(); return sizeof(Message); } static int message_scm_print(SCM msg_smob, SCM port, scm_print_state* pstate) { scm_puts("#path().c_str(), port); scm_puts(">", port); return 1; } struct FlagData { Flags flags; SCM lst; }; #define MU_GUILE_INITIALIZED_OR_ERROR \ do { \ if (!(mu_guile_initialized())) { \ mu_guile_error(FUNC_NAME, \ 0, \ "mu not initialized; call mu:initialize", \ SCM_UNDEFINED); \ return SCM_UNSPECIFIED; \ } \ } while (0) static SCM get_flags_scm(const Message& msg) { SCM lst{SCM_EOL}; const auto flags{msg.flags()}; for (auto i = 0; i != AllMessageFlagInfos.size(); ++i) { const auto& info{AllMessageFlagInfos.at(i)}; if (any_of(info.flag & flags)) scm_append_x(scm_list_2(lst, scm_list_1(SYMB_FLAGS.at(i)))); } return lst; } static SCM get_prio_scm(const Message& msg) { switch (msg.priority()) { case Priority::Low: return SYMB_PRIO_LOW; case Priority::Normal: return SYMB_PRIO_NORMAL; case Priority::High: return SYMB_PRIO_HIGH; default: g_return_val_if_reached(SCM_UNDEFINED); } } static SCM msg_string_list_field(const Message& msg, Field::Id field_id) { SCM scmlst{SCM_EOL}; for (auto&& val: msg.document().string_vec_value(field_id)) { SCM item; item = scm_list_1(mu_guile_scm_from_string(val)); scmlst = scm_append_x(scm_list_2(scmlst, item)); } return scmlst; } static SCM msg_contact_list_field(const Message& msg, Field::Id field_id) { return scm_from_utf8_string( to_string(msg.document().contacts_value(field_id)).c_str()); } static SCM get_body(const Message& msg, bool html) { if (const auto body = html ? msg.body_html() : msg.body_text(); body) return mu_guile_scm_from_string(*body); else return SCM_BOOL_F; } 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 { SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); auto msg{message_from_scm(MSG)}; SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME); SCM_ASSERT(scm_integer_p(FIELD), FIELD, SCM_ARG2, FUNC_NAME); const auto field_opt{field_from_number(static_cast(scm_to_int(FIELD)))}; SCM_ASSERT(!!field_opt, FIELD, SCM_ARG2, FUNC_NAME); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" switch (field_opt->id) { case Field::Id::Priority: return get_prio_scm(*msg); case Field::Id::Flags: return get_flags_scm(*msg); case Field::Id::BodyText: return get_body(*msg, false); default: break; } #pragma GCC diagnostic pop switch (field_opt->type) { case Field::Type::String: return mu_guile_scm_from_string(msg->document().string_value(field_opt->id)); case Field::Type::ByteSize: case Field::Type::TimeT: case Field::Type::Integer: return scm_from_uint(msg->document().integer_value(field_opt->id)); case Field::Type::StringList: return msg_string_list_field(*msg, field_opt->id); case Field::Type::ContactList: return msg_contact_list_field(*msg, field_opt->id); default: SCM_ASSERT(0, FIELD, SCM_ARG2, FUNC_NAME); } return SCM_UNSPECIFIED; } #undef FUNC_NAME static SCM contacts_to_list(const Message& msg, Option field_id) { SCM list{SCM_EOL}; const auto contacts{field_id ? msg.document().contacts_value(*field_id) : msg.all_contacts()}; for (auto&& contact: contacts) { SCM item{scm_list_1( scm_cons(mu_guile_scm_from_string(contact.name), mu_guile_scm_from_string(contact.email)))}; list = scm_append_x(scm_list_2(list, item)); } return list; } 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 { SCM list; MU_GUILE_INITIALIZED_OR_ERROR; SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); auto msg{message_from_scm(MSG)}; SCM_ASSERT(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 */ Option field_id; if (CONTACT_TYPE == SCM_BOOL_T) field_id = {}; /* get all */ else { if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_TO)) field_id = Field::Id::To; else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_CC)) field_id = Field::Id::Cc; else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_BCC)) field_id = Field::Id::Bcc; else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_FROM)) field_id = Field::Id::From; else { mu_guile_error(FUNC_NAME, 0, "invalid contact type", SCM_UNDEFINED); return SCM_UNSPECIFIED; } } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-function-type" list = contacts_to_list(*msg, field_id); #pragma GCC diagnostic pop /* explicitly close the file backend, so we won't run out of fds */ return list; } #undef FUNC_NAME 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 { MU_GUILE_INITIALIZED_OR_ERROR; SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); auto msg{message_from_scm(MSG)}; SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME); SCM_ASSERT(scm_is_bool(ATTS_ONLY), ATTS_ONLY, SCM_ARG2, FUNC_NAME); SCM attlist = SCM_EOL; /* empty list */ bool attachments_only = ATTS_ONLY == SCM_BOOL_T ? TRUE : FALSE; size_t n{}; for (auto&& part: msg->parts()) { if (attachments_only && !part.is_attachment()) continue; const auto mime_type{part.mime_type()}; const auto filename{part.cooked_filename()}; SCM elm = scm_list_5( /* msg */ mu_guile_scm_from_string(msg->path().c_str()), /* index */ scm_from_uint(n++), /* filename or #f */ filename ? mu_guile_scm_from_string(*filename) : SCM_BOOL_F, /* mime-type */ mime_type ? mu_guile_scm_from_string(*mime_type) : SCM_BOOL_F, /* size */ part.size() > 0 ? scm_from_uint(part.size()) : SCM_BOOL_F); attlist = scm_cons(elm, attlist); } /* explicitly close the file backend, so we won't run of fds */ msg->unload_mime_message(); return 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 { MU_GUILE_INITIALIZED_OR_ERROR; SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); auto msg{message_from_scm(MSG)}; SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME); SCM_ASSERT(scm_is_string(HEADER) || HEADER == SCM_UNDEFINED, HEADER, SCM_ARG2, FUNC_NAME); char *header = scm_to_utf8_string(HEADER); SCM val = mu_guile_scm_from_string(msg->header(header).value_or("")); free(header); /* explicitly close the file backend, so we won't run of fds */ msg->unload_mime_message(); return val; } #undef FUNC_NAME 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 { 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); const auto res = mu_guile_store().run_query(expr,{}, {}, scm_to_int(MAXNUM)); free(expr); if (!res) return SCM_UNSPECIFIED; for (auto&& mi : *res) { if (auto xdoc{mi.document()}; xdoc) { scm_call_1(FUNC, message_scm_create(std::move(xdoc.value()))); } } 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"); for (auto i = 0U; i != AllMessageFlagInfos.size(); ++i) { const auto& info{AllMessageFlagInfos.at(i)}; const auto name = "mu:flag:" + std::string{info.name}; SYMB_FLAGS[i] = register_symbol(name.c_str()); } } static void define_vars(void) { field_for_each([](auto&& field){ auto defvar = [&](auto&& fname, auto&& ffield) { const auto name{"mu:field:" + std::string{fname}}; scm_c_define(name.c_str(), scm_from_uint(field.value_no())); scm_c_export(name.c_str(), NULL); }; // define for both name and (if exists) alias. if (!field.name.empty()) defvar(field.name, field); if (!field.alias.empty()) defvar(field.alias, field); }); /* non-Xapian field: timestamp */ scm_c_define("mu:field:timestamp", scm_from_uint(MU_GUILE_MSG_FIELD_ID_TIMESTAMP)); scm_c_export("mu:field:timestamp", NULL); } void* mu_guile_message_init(void* data) { MSG_TAG = scm_make_smob_type("message", sizeof(Message)); scm_set_smob_free(MSG_TAG, message_scm_free); scm_set_smob_print(MSG_TAG, message_scm_print); define_vars(); define_symbols(); #ifndef SCM_MAGIC_SNARFER #include "mu-guile-message.x" #endif /*SCM_MAGIC_SNARFER*/ return NULL; } mu-1.10.8/guile/mu-guile-message.hh000066400000000000000000000017641452323006500170260ustar00rootroot00000000000000/* ** 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. ** */ #ifndef MU_GUILE_MESSAGE_H__ #define MU_GUILE_MESSAGE_H__ /** * Initialize this mu guile module. * * @param data q * * @return */ extern "C" { void* mu_guile_message_init(void* data); } #endif /*MU_GUILE_MESSAGE_HH__*/ mu-1.10.8/guile/mu-guile-message.x000066400000000000000000000024041452323006500166660ustar00rootroot00000000000000/* cpp arguments: mu-guile-message.cc -DHAVE_CONFIG_H -I. -I.. -I../lib -I/usr/local/include/guile/3.0 -pthread -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/libmount -I/usr/include/blkid -pthread -fno-strict-aliasing -Wall -Wextra -Wundef -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wformat=2 -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wreturn-type -Wno-overloaded-virtual -Wswitch-enum -Wswitch-default -Wno-error=unused-parameter -Wno-error=missing-field-initializers -Wno-error=overloaded-virtual -Wno-redundant-decls -Wno-missing-declarations -Wno-suggest-attribute=noreturn -O2 -Wno-inline */ scm_c_define_gsubr (s_get_field, 2, 0, 0, (scm_t_subr) get_field);; scm_c_define_gsubr (s_get_contacts, 2, 0, 0, (scm_t_subr) get_contacts);; scm_c_define_gsubr (s_get_parts, 1, 1, 0, (scm_t_subr) get_parts);; scm_c_define_gsubr (s_get_header, 2, 0, 0, (scm_t_subr) get_header);; scm_c_define_gsubr (s_for_each_message, 3, 0, 0, (scm_t_subr) for_each_message);; mu-1.10.8/guile/mu-guile.cc000066400000000000000000000140301452323006500153600ustar00rootroot00000000000000/* ** Copyright (C) 2011-2022 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-guile.hh" #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wredundant-decls" #include #pragma GCC diagnostic pop #include #include #include using namespace Mu; SCM mu_guile_scm_from_string(const std::string& str) { if (str.empty()) return SCM_BOOL_F; else return scm_from_stringn(str.c_str(), str.size(), "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 Option StoreSingleton = Nothing; static bool mu_guile_init_instance(const std::string& muhome) try { setlocale(LC_ALL, ""); const auto path{runtime_path(RuntimePath::XapianDb, muhome)}; auto store = Store::make(path); if (!store) { g_critical("error creating store @ %s: %s", path.c_str(), store.error().what()); throw store.error(); } else StoreSingleton.emplace(std::move(store.value())); g_debug("mu-guile: opened store @ %s (n=%zu); maildir: %s", StoreSingleton->properties().database_path.c_str(), StoreSingleton->size(), StoreSingleton->properties().root_maildir.c_str()); return true; } catch (const Xapian::Error& xerr) { g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str()); return false; } catch (const std::runtime_error& re) { g_critical("%s: error: %s", __func__, re.what()); return false; } catch (const std::exception& e) { g_critical("%s: caught exception: %s", __func__, e.what()); return false; } catch (...) { g_critical("%s: caught exception", __func__); return false; } static void mu_guile_uninit_instance() { StoreSingleton.reset(); } Mu::Store& mu_guile_store() { if (!StoreSingleton) g_error("mu guile not initialized"); return StoreSingleton.value(); } gboolean mu_guile_initialized() { g_debug("initialized ? %u", !!StoreSingleton); return !!StoreSingleton; } 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; 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); if (!mu_guile_init_instance(muhome ? muhome : "")) { free(muhome); mu_guile_error(FUNC_NAME, 0, "Failed to initialize mu", SCM_UNSPECIFIED); return SCM_UNSPECIFIED; } g_debug("mu-guile: initialized @ %s", muhome ? muhome : ""); free(muhome); /* 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, (GLogLevelFlags)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; } mu-1.10.8/guile/mu-guile.hh000066400000000000000000000037161452323006500154030ustar00rootroot00000000000000/* ** 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. ** */ #ifndef __MU_GUILE_H__ #define __MU_GUILE_H__ #include #include #include /** * get the singleton Store instance */ Mu::Store& mu_guile_store(); /** * whether mu-guile is initialized * * @return TRUE if MuGuile is Initialized, FALSE otherwise */ gboolean mu_guile_initialized(); /** * 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 string into an SCM -- . It assumes str is in UTF8 encoding, and * replace characters with '?' if needed. * * @param str a string * * @return a guile string or #f for empty */ SCM mu_guile_scm_from_string(const std::string& str); /** * Initialize this mu guile module. * * @param data * * @return */ extern "C" { void* mu_guile_init(void* data); } #endif /*__MU_GUILE_H__*/ mu-1.10.8/guile/mu-guile.texi000066400000000000000000001007261452323006500157540ustar00rootroot00000000000000\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 version.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{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 ������������������������������������������mu-1.10.8/guile/mu-guile.x��������������������������������������������������������������������������0000664�0000000�0000000�00000002315�14523230065�0015245�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* cpp arguments: mu-guile.cc -DHAVE_CONFIG_H -I. -I.. -I../lib -I/usr/local/include/guile/3.0 -pthread -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/libmount -I/usr/include/blkid -pthread -fno-strict-aliasing -Wall -Wextra -Wundef -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wformat=2 -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wreturn-type -Wno-overloaded-virtual -Wswitch-enum -Wswitch-default -Wno-error=unused-parameter -Wno-error=missing-field-initializers -Wno-error=overloaded-virtual -Wno-redundant-decls -Wno-missing-declarations -Wno-suggest-attribute=noreturn -O2 -Wno-inline */ scm_c_define_gsubr (s_mu_initialize, 0, 1, 0, (scm_t_subr) mu_initialize); scm_c_export (s_mu_initialize, __null );; scm_c_define_gsubr (s_mu_initialized_p, 0, 0, 0, (scm_t_subr) mu_initialized_p); scm_c_export (s_mu_initialized_p, __null );; scm_c_define_gsubr (s_log_func, 1, 0, 1, (scm_t_subr) log_func);; �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu.scm������������������������������������������������������������������������������0000664�0000000�0000000�00000024527�14523230065�0014466�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������;; 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)) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu/���������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0013750�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu/README���������������������������������������������������������������������������0000664�0000000�0000000�00000015023�14523230065�0014631�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������* 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: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu/contact.scm����������������������������������������������������������������������0000664�0000000�0000000�00000000205�14523230065�0016104�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������(define-module (mu contact) :use-module(mu)) (display "(mu contact) is deprecated, please remove from (use-modules ...)") (newline) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu/message.scm����������������������������������������������������������������������0000664�0000000�0000000�00000000206�14523230065�0016076�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������(define-module (mu message) :use-module (mu)) (display "(mu message) is deprecated, please remove from (use-modules ...)") (newline) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu/part.scm�������������������������������������������������������������������������0000664�0000000�0000000�00000000200�14523230065�0015412�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������(define-module (mu part) :use-module (mu)) (display "(mu part) is deprecated, please remove from (use-modules ...)") (newline) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu/plot.scm�������������������������������������������������������������������������0000664�0000000�0000000�00000005561�14523230065�0015441�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������;; ;; Copyright (C) 2011-2022 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* ((output (mkstemp "/tmp/mu-guile-XXXXXX" "w")) (datafile (port-filename output))) (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")) (when (zero? (length data)) (error "No data for plotting")) (let* ((datafile (export-pairs data)) (gnuplot (open-pipe "gnuplot -p" OPEN_WRITE)) (recipe (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 title \"\"\n"))) (display recipe gnuplot) (close-pipe gnuplot))) ;; backward compatibility (define mu:plot mu:plot-histogram) �����������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu/script.scm�����������������������������������������������������������������������0000664�0000000�0000000�00000004131�14523230065�0015757�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������;; 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)) (time-unit (value #t)) ;; Ignore. (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: ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/mu/stats.scm������������������������������������������������������������������������0000664�0000000�0000000�00000013403�14523230065�0015613�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������;; ;; Copyright (C) 2011-2022 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)) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/scripts/����������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0015016�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/scripts/find-dups.scm���������������������������������������������������������������0000775�0000000�0000000�00000007251�14523230065�0017423�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/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: �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/scripts/histogram.scm���������������������������������������������������������������0000775�0000000�0000000�00000010241�14523230065�0017520�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh exec guile -e main -s $0 $@ !# ;; Copyright (C) 2022 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. ;; INFO: Histogram of the number of messages per time-unit ;; INFO: Options: ;; INFO: --query=<query>: limit to messages matching query ;; INFO: --muhome=<muhome>: path to mu home dir ;; INFO: --time-unit: hour|day|month|year|month-year ;; INFO: --output: the output format, such as "png", "wxt" ;; INFO: (depending on the environment) (use-modules (mu) (mu stats) (mu plot) (ice-9 getopt-long) (ice-9 format)) (define (per-hour expr output) "Count the total number of messages per hour 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 (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 (per-month expr output) "Count the total number of messages per month 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 (per-year expr output) "Count the total number of messages per year 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 (per-year-month expr output) "Count the total number of messages for each year and month 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) (let* ((optionspec '((time-unit (value #t)) (query (value #t)) (muhome (value #t)) (output (value #t)) (help (single-char #\h) (value #f)))) (options (getopt-long args optionspec)) (help (option-ref options 'help #f)) (time-unit (option-ref options 'time-unit "year")) (muhome (option-ref options 'muhome #f)) (query (option-ref options 'query "")) (output (option-ref options 'output "dumb")) (rest (option-ref options '() #f)) (func (cond ((equal? time-unit "hour") per-hour) ((equal? time-unit "day") per-day) ((equal? time-unit "month") per-month) ((equal? time-unit "year") per-year) ((equal? time-unit "year-month") per-year-month) (else #f)))) (setlocale LC_ALL "") (unless func (display "error: unknown time-unit\n") (set! help #t)) (if help (begin (display (string-append "parameters: [--help] [--output=dumb|png|wxt] " "[--muhome=<muhome>] [--query=<query>]" "[--time-unit=hour|day|month|year|year-month]")) (newline)) (begin (mu:initialize muhome) (func query output))))) ;; Local Variables: ;; mode: scheme ;; End: ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/scripts/msgs-count.scm��������������������������������������������������������������0000775�0000000�0000000�00000002432�14523230065�0017625�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh exec guile -e main -s $0 $@ !# ;; Copyright (C) 2022 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: Count the number of messages matching some query ;; INFO: options: ;; INFO: --query=<query>: limit to messages matching query ;; INFO: --muhome=<muhome>: path to mu home dir (optional) (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: ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/tests/������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0014471�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/guile/tests/test-mu-guile.cc��������������������������������������������������������������0000664�0000000�0000000�00000005742�14523230065�0017511�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2012-2022 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 <glib/gstdio.h> #include <lib/mu-query.hh> #include <stdlib.h> #include <unistd.h> #include <string.h> #include "utils/mu-test-utils.hh" #include <lib/mu-store.hh> #include <utils/mu-utils.hh> using namespace Mu; static std::string test_dir; static std::string fill_database(void) { const auto cmdline = format( "/bin/sh -c '" "%s init --muhome=%s --maildir=%s --quiet; " "%s index --muhome=%s --quiet'", MU_PROGRAM, test_dir.c_str(), MU_TESTMAILDIR2, MU_PROGRAM, test_dir.c_str()); if (g_test_verbose()) g_print("%s\n", cmdline.c_str()); GError *err{}; if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, NULL, &err)) { g_printerr("Error: %s\n", err ? err->message : "?"); g_clear_error(&err); g_assert(0); } return test_dir; } static void test_something(const char* what) { g_setenv("GUILE_AUTO_COMPILE", "0", TRUE); g_setenv("GUILE_LOAD_PATH", GUILE_LOAD_PATH, TRUE); g_setenv("GUILE_EXTENSIONS_PATH",GUILE_EXTENSIONS_PATH, TRUE); if (g_test_verbose()) g_print("GUILE_LOAD_PATH: %s\n", GUILE_LOAD_PATH); const auto dir = fill_database(); const auto cmdline = format("%s -q -e main %s/test-mu-guile.scm " "--muhome=%s --test=%s", GUILE_BINARY, ABS_SRCDIR, dir.c_str(), what); if (g_test_verbose()) g_print("cmdline: %s\n", cmdline.c_str()); GError *err{}; int status{}; if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, &status, &err) || status != 0) { g_printerr("Error: %s\n", err ? err->message : "something went wrong"); g_clear_error(&err); g_assert(0); } } 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; TempDir tempdir; test_dir = tempdir.path(); mu_test_init(&argc, &argv); 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); rv = g_test_run(); return rv; } ������������������������������mu-1.10.8/guile/tests/test-mu-guile.scm�������������������������������������������������������������0000775�0000000�0000000�00000010414�14523230065�0017701�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/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: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/��������������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0012770�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/doxyfile.in���������������������������������������������������������������������������0000664�0000000�0000000�00000015576�14523230065�0015161�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 ����������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/index/��������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0014077�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/index/meson.build���������������������������������������������������������������������0000664�0000000�0000000�00000002424�14523230065�0016243�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Copyright (C) 2021 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. index_srcs=[ 'mu-indexer.hh', 'mu-indexer.cc', 'mu-scanner.hh', 'mu-scanner.cc' ] xapian_incs = xapian_dep.get_variable(pkgconfig: 'includedir') lib_mu_index_inc_dep = declare_dependency( include_directories: include_directories(['.', '..', xapian_incs])) lib_mu_index=static_library('mu-index', [index_srcs], dependencies: [ config_h_dep, glib_dep, lib_mu_index_inc_dep ], install: false) lib_mu_index_dep = declare_dependency( link_with: lib_mu_index ) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/index/mu-indexer.cc�������������������������������������������������������������������0000664�0000000�0000000�00000025352�14523230065�0016472�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2020-2022 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-indexer.hh" #include <config.h> #include <atomic> #include <algorithm> #include <mutex> #include <vector> #include <thread> #include <condition_variable> #include <iostream> #include <atomic> #include <chrono> using namespace std::chrono_literals; #include "mu-scanner.hh" #include "utils/mu-async-queue.hh" #include "utils/mu-error.hh" #include "../mu-store.hh" using namespace Mu; struct IndexState { enum State { Idle, Scanning, Finishing, Cleaning }; static const char* name(State s) { switch (s) { case Idle: return "idle"; case Scanning: return "scanning"; case Finishing: return "finishing"; case Cleaning: return "cleaning"; default: return "<error>"; } } bool operator==(State rhs) const { return state_.load() == rhs; } bool operator!=(State rhs) const { return state_.load() != rhs; } void change_to(State new_state) { g_debug("changing indexer state %s->%s", name((State)state_), name((State)new_state)); state_.store(new_state); } private: std::atomic<State> state_{Idle}; }; struct Indexer::Private { Private(Mu::Store& store) : store_{store}, scanner_{store_.properties().root_maildir, [this](auto&& path, auto&& statbuf, auto&& info) { return handler(path, statbuf, info); }}, max_message_size_{store_.properties().max_message_size} { g_message("created indexer for %s -> %s (batch-size: %zu)", store.properties().root_maildir.c_str(), store.properties().database_path.c_str(), store.properties().batch_size); } ~Private() { stop(); } bool dir_predicate(const std::string& path, const struct dirent* dirent) const; bool handler(const std::string& fullpath, struct stat* statbuf, Scanner::HandleType htype); void maybe_start_worker(); void item_worker(); void scan_worker(); bool add_message(const std::string& path); bool cleanup(); bool start(const Indexer::Config& conf); bool stop(); Indexer::Config conf_; Store& store_; Scanner scanner_; const size_t max_message_size_; time_t dirstamp_{}; std::size_t max_workers_; std::vector<std::thread> workers_; std::thread scanner_worker_; struct WorkItem { std::string full_path; enum Type { Dir, File }; Type type; }; AsyncQueue<WorkItem> todos_; Progress progress_; IndexState state_; std::mutex lock_, w_lock_; std::atomic<time_t> completed_; }; bool Indexer::Private::handler(const std::string& fullpath, struct stat* statbuf, Scanner::HandleType htype) { switch (htype) { case Scanner::HandleType::EnterDir: case Scanner::HandleType::EnterNewCur: { // in lazy-mode, we ignore this dir if its dirstamp suggest it // is up-to-date (this is _not_ always true; hence we call it // lazy-mode); only for actual message dirs, since the dir // tstamps may not bubble up. dirstamp_ = store_.dirstamp(fullpath); if (conf_.lazy_check && dirstamp_ >= statbuf->st_ctime && htype == Scanner::HandleType::EnterNewCur) { g_debug("skip %s (seems up-to-date: %s >= %s)", fullpath.c_str(), time_to_string("%FT%T", dirstamp_).c_str(), time_to_string("%FT%T", statbuf->st_ctime).c_str()); return false; } // don't index dirs with '.noindex' auto noindex = ::access((fullpath + "/.noindex").c_str(), F_OK) == 0; if (noindex) { g_debug("skip %s (has .noindex)", fullpath.c_str()); return false; // don't descend into this dir. } // don't index dirs with '.noupdate', unless we do a full // (re)index. if (!conf_.ignore_noupdate) { auto noupdate = ::access((fullpath + "/.noupdate").c_str(), F_OK) == 0; if (noupdate) { g_debug("skip %s (has .noupdate)", fullpath.c_str()); return false; } } g_debug("checked %s", fullpath.c_str()); return true; } case Scanner::HandleType::LeaveDir: { todos_.push({fullpath, WorkItem::Type::Dir}); return true; } case Scanner::HandleType::File: { ++progress_.checked; if ((size_t)statbuf->st_size > max_message_size_) { g_debug("skip %s (too big: %" G_GINT64_FORMAT " bytes)", fullpath.c_str(), (gint64)statbuf->st_size); return false; } // if the message is not in the db yet, or not up-to-date, queue // it for updating/inserting. if (statbuf->st_ctime <= dirstamp_ && store_.contains_message(fullpath)) { // g_debug ("skip %s: already up-to-date"); return false; } // push the remaining messages to our "todo" queue for // (re)parsing and adding/updating to the database. todos_.push({fullpath, WorkItem::Type::File}); return true; } default: g_return_val_if_reached(false); return false; } } void Indexer::Private::maybe_start_worker() { std::lock_guard lock{w_lock_}; if (todos_.size() > workers_.size() && workers_.size() < max_workers_) { workers_.emplace_back(std::thread([this] { item_worker(); })); g_debug("added worker %zu", workers_.size()); } } bool Indexer::Private::add_message(const std::string& path) { /* * Having the lock here makes things a _lot_ slower. * * The reason for having the lock is some helgrind warnings; * but it believed those are _false alarms_ * https://gitlab.gnome.org/GNOME/glib/-/issues/2662 * * std::unique_lock lock{w_lock_}; */ auto msg{Message::make_from_path(path)}; if (!msg) { g_warning("failed to create message from %s: %s", path.c_str(), msg.error().what()); return false; } auto res = store_.add_message(msg.value(), true /*use-transaction*/); if (!res) { g_warning("failed to add message @ %s: %s", path.c_str(), res.error().what()); return false; } return true; } void Indexer::Private::item_worker() { WorkItem item; g_debug("started worker"); while (state_ == IndexState::Scanning) { if (!todos_.pop(item, 250ms)) continue; try { switch (item.type) { case WorkItem::Type::File: { if (G_LIKELY(add_message(item.full_path))) ++progress_.updated; } break; case WorkItem::Type::Dir: store_.set_dirstamp(item.full_path, ::time(NULL)); break; default: g_warn_if_reached(); break; } } catch (const Mu::Error& er) { g_warning("error adding message @ %s: %s", item.full_path.c_str(), er.what()); } maybe_start_worker(); std::this_thread::yield(); } } bool Indexer::Private::cleanup() { g_debug("starting cleanup"); size_t n{}; std::vector<Store::Id> orphans; // store messages without files. store_.for_each_message_path([&](Store::Id id, const std::string& path) { ++n; if (::access(path.c_str(), R_OK) != 0) { g_debug("cannot read %s (id=%u); queueing for removal from store", path.c_str(), id); orphans.emplace_back(id); } return state_ == IndexState::Cleaning; }); if (orphans.empty()) g_debug("nothing to clean up"); else { g_debug("removing up %zu stale message(s) from store", orphans.size()); store_.remove_messages(orphans); progress_.removed += orphans.size(); } return true; } void Indexer::Private::scan_worker() { progress_.reset(); if (conf_.scan) { g_debug("starting scanner"); if (!scanner_.start()) { // blocks. g_warning("failed to start scanner"); state_.change_to(IndexState::Idle); return; } g_debug("scanner finished with %zu file(s) in queue", todos_.size()); } // now there may still be messages in the work queue... // finish those; this is a bit ugly; perhaps we should // handle SIGTERM etc. if (!todos_.empty()) { const auto workers_size = std::invoke([this] { std::lock_guard lock{w_lock_}; return workers_.size(); }); g_debug("process %zu remaining message(s) with %zu worker(s)", todos_.size(), workers_size); while (!todos_.empty()) std::this_thread::sleep_for(100ms); } // and let the worker finish their work. state_.change_to(IndexState::Finishing); for (auto&& w : workers_) if (w.joinable()) w.join(); if (conf_.cleanup) { g_debug("starting cleanup"); state_.change_to(IndexState::Cleaning); cleanup(); g_debug("cleanup finished"); } completed_ = ::time({}); state_.change_to(IndexState::Idle); } bool Indexer::Private::start(const Indexer::Config& conf) { stop(); conf_ = conf; if (conf_.max_threads == 0) { /* benchmarking suggests that ~4 threads is the fastest (the * real bottleneck is the database, so adding more threads just * slows things down) */ max_workers_ = std::min(4U, std::thread::hardware_concurrency()); } else max_workers_ = conf.max_threads; g_debug("starting indexer with <= %zu worker thread(s)", max_workers_); g_debug("indexing: %s; clean-up: %s", conf_.scan ? "yes" : "no", conf_.cleanup ? "yes" : "no"); state_.change_to(IndexState::Scanning); /* kick off the first worker, which will spawn more if needed. */ workers_.emplace_back(std::thread([this] { item_worker(); })); /* kick the disk-scanner thread */ scanner_worker_ = std::thread([this] { scan_worker(); }); g_debug("started indexer"); return true; } bool Indexer::Private::stop() { scanner_.stop(); todos_.clear(); if (scanner_worker_.joinable()) scanner_worker_.join(); state_.change_to(IndexState::Idle); for (auto&& w : workers_) if (w.joinable()) w.join(); workers_.clear(); return true; } Indexer::Indexer(Store& store) : priv_{std::make_unique<Private>(store)} {} Indexer::~Indexer() = default; bool Indexer::start(const Indexer::Config& conf) { const auto mdir{priv_->store_.properties().root_maildir}; if (G_UNLIKELY(access(mdir.c_str(), R_OK) != 0)) { g_critical("'%s' is not readable: %s", mdir.c_str(), g_strerror(errno)); return false; } std::lock_guard lock(priv_->lock_); if (is_running()) return true; return priv_->start(conf); } bool Indexer::stop() { std::lock_guard lock{priv_->lock_}; if (!is_running()) return true; g_debug("stopping indexer"); return priv_->stop(); } bool Indexer::is_running() const { return priv_->state_ != IndexState::Idle; } const Indexer::Progress& Indexer::progress() const { priv_->progress_.running = priv_->state_ == IndexState::Idle ? false : true; return priv_->progress_; } time_t Indexer::completed() const { return priv_->completed_; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/index/mu-indexer.hh�������������������������������������������������������������������0000664�0000000�0000000�00000006001�14523230065�0016472�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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_INDEXER_HH__ #define MU_INDEXER_HH__ #include <atomic> #include <memory> #include <chrono> namespace Mu { class Store; /// An object abstracting the index process. class Indexer { public: /** * Construct an indexer object * * @param store the message store to use */ Indexer(Store& store); /** * DTOR */ ~Indexer(); /// A configuration object for the indexer struct Config { bool scan{true}; /**< scan for new messages */ bool cleanup{true}; /**< clean messages no longer in the file system */ size_t max_threads{}; /**< maximum # of threads to use */ bool ignore_noupdate{}; /**< ignore .noupdate files */ bool lazy_check{}; /**< whether to skip directories that don't have a changed * mtime */ }; /** * Start indexing. If already underway, do nothing. This returns * immediately after starting, with the work being done in the * background. * * @param conf a configuration object * * @return true if starting worked or an indexing process was already * underway; false otherwise. * */ bool start(const Config& conf); /** * Stop indexing. If not indexing, do nothing. * * * @return true if we stopped indexing, or indexing was not underway. * False otherwise. */ bool stop(); /** * Is an indexing process running? * * @return true or false. */ bool is_running() const; // Object describing current progress struct Progress { void reset() { running = false; checked = updated = removed = 0; } std::atomic<bool> running{}; /**< Is an index operation in progress? */ std::atomic<size_t> checked{}; /**< Number of messages checked for changes */ std::atomic<size_t> updated{}; /**< Number of messages (re)parsed/added/updated */ std::atomic<size_t> removed{}; /**< Number of message removed from store */ }; /** * Get an object describing the current progress. The progress object * describes the most recent indexing job, and is reset upon a fresh * start(). * * @return a progress object. */ const Progress& progress() const; /** * Last time indexing was completed. * * @return the time or 0 */ time_t completed() const; private: struct Private; std::unique_ptr<Private> priv_; }; } // namespace Mu #endif /* MU_INDEXER_HH__ */ �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/index/mu-scanner.cc�������������������������������������������������������������������0000664�0000000�0000000�00000013547�14523230065�0016470�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2020-2023 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-scanner.hh" #include "config.h" #include <chrono> #include <mutex> #include <atomic> #include <thread> #include <cstring> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <glib.h> #include "utils/mu-utils.hh" #include "utils/mu-utils-file.hh" #include "utils/mu-error.hh" using namespace Mu; struct Scanner::Private { Private(const std::string& root_dir, Scanner::Handler handler) : root_dir_{root_dir}, handler_{handler} { if (!handler_) throw Mu::Error{Error::Code::Internal, "missing handler"}; } ~Private() { stop(); } bool start(); bool stop(); bool process_dentry(const std::string& path, struct dirent* dentry, bool is_maildir); bool process_dir(const std::string& path, bool is_maildir); const std::string root_dir_; const Scanner::Handler handler_; std::atomic<bool> running_{}; std::mutex lock_; }; static bool is_dotdir(const char *d_name) { /* dotdir? */ if (d_name[0] == '\0' || (d_name[1] == '\0' && d_name[0] == '.') || (d_name[2] == '\0' && d_name[0] == '.' && d_name[1] == '.')) return true; return false; } static bool do_ignore(const char *d_name) { if (d_name[0] == '.') { if (d_name[1] == '#') /* emacs? */ return true; if (g_strcmp0(d_name + 1, "nnmaildir") == 0) /* gnus? */ return true; if (g_strcmp0(d_name + 1, "notmuch") == 0) /* notmuch? */ return true; } if (g_strcmp0(d_name, "hcache.db") == 0) /* mutt cache? */ return true; return false; } bool Scanner::Private::process_dentry(const std::string& path, struct dirent *dentry, bool is_maildir) { const auto d_name{dentry->d_name}; if (is_dotdir(d_name) || std::strcmp(d_name, "tmp") == 0) return true; // ignore. if (do_ignore(d_name)) { g_debug("skip %s/%s (ignore)", path.c_str(), d_name); return true; // ignore } const auto fullpath{join_paths(path, d_name)}; struct stat statbuf {}; if (::stat(fullpath.c_str(), &statbuf) != 0) { g_warning("failed to stat %s: %s", fullpath.c_str(), g_strerror(errno)); return false; } if (S_ISDIR(statbuf.st_mode)) { const auto new_cur = std::strcmp(d_name, "cur") == 0 || std::strcmp(d_name, "new") == 0; const auto htype = new_cur ? Scanner::HandleType::EnterNewCur : Scanner::HandleType::EnterDir; const auto res = handler_(fullpath, &statbuf, htype); if (!res) return true; // skip process_dir(fullpath, new_cur); return handler_(fullpath, &statbuf, Scanner::HandleType::LeaveDir); } else if (S_ISREG(statbuf.st_mode) && is_maildir) return handler_(fullpath, &statbuf, Scanner::HandleType::File); g_debug("skip %s (neither maildir-file nor directory)", fullpath.c_str()); return true; } bool Scanner::Private::process_dir(const std::string& path, bool is_maildir) { if (!running_) return true; /* we're done */ const auto dir = opendir(path.c_str()); if (G_UNLIKELY(!dir)) { g_warning("failed to scan dir %s: %s", path.c_str(), g_strerror(errno)); return false; } // TODO: sort dentries by inode order, which makes things faster for extfs. // see mu-maildir.c while (running_) { errno = 0; const auto dentry{readdir(dir)}; if (G_LIKELY(dentry)) { process_dentry(path, dentry, is_maildir); continue; } if (errno != 0) { g_warning("failed to read %s: %s", path.c_str(), g_strerror(errno)); continue; } break; } closedir(dir); return true; } bool Scanner::Private::start() { const auto& path{root_dir_}; if (G_UNLIKELY(path.length() > PATH_MAX)) { g_warning("path too long"); return false; } const auto mode{F_OK | R_OK}; if (G_UNLIKELY(access(path.c_str(), mode) != 0)) { g_warning("'%s' is not readable: %s", path.c_str(), g_strerror(errno)); return false; } struct stat statbuf { }; if (G_UNLIKELY(stat(path.c_str(), &statbuf) != 0)) { g_warning("'%s' is not stat'able: %s", path.c_str(), g_strerror(errno)); return false; } if (G_UNLIKELY(!S_ISDIR(statbuf.st_mode))) { g_warning("'%s' is not a directory", path.c_str()); return false; } running_ = true; g_debug("starting scan @ %s", root_dir_.c_str()); auto basename{g_path_get_basename(root_dir_.c_str())}; const auto is_maildir = (g_strcmp0(basename, "cur") == 0 || g_strcmp0(basename, "new") == 0); g_free(basename); const auto start{std::chrono::steady_clock::now()}; process_dir(root_dir_, is_maildir); const auto elapsed = std::chrono::steady_clock::now() - start; g_debug("finished scan of %s in %" G_GINT64_FORMAT " ms", root_dir_.c_str(), to_ms(elapsed)); running_ = false; return true; } bool Scanner::Private::stop() { if (!running_) return true; // nothing to do g_debug("stopping scan"); running_ = false; return true; } Scanner::Scanner(const std::string& root_dir, Scanner::Handler handler) : priv_{std::make_unique<Private>(root_dir, handler)} { } Scanner::~Scanner() = default; bool Scanner::start() { if (priv_->running_) return true; // nothing to do const auto res = priv_->start(); /* blocks */ priv_->running_ = false; return res; } bool Scanner::stop() { std::lock_guard l(priv_->lock_); return priv_->stop(); } bool Scanner::is_running() const { return priv_->running_; } ���������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/index/mu-scanner.hh�������������������������������������������������������������������0000664�0000000�0000000�00000004576�14523230065�0016504�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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_SCANNER_HH__ #define MU_SCANNER_HH__ #include <functional> #include <memory> #include <dirent.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> namespace Mu { /// @brief Maildir scanner /// /// Scans maildir (trees) recursively, and calls the Handler callback for /// directories & files. /// /// It filters out (i.e., does *not* call the handler for): /// - files starting with '.' /// - files that do not live in a cur / new leaf maildir /// - directories '.' and '..' and 'tmp' /// class Scanner { public: enum struct HandleType { File, EnterNewCur, /* cur/ or new/ */ EnterDir, /* some other directory */ LeaveDir }; /// Prototype for a handler function using Handler = std::function< bool(const std::string& fullpath, struct stat* statbuf, HandleType htype)>; /** * Construct a scanner object for scanning a directory, recursively. * * If handler is a directory * * * @param root_dir root dir to start scanning * @param handler handler function for some direntry */ Scanner(const std::string& root_dir, Handler handler); /** * DTOR */ ~Scanner(); /** * Start the scan; this is a blocking call than runs until * finished or (from another thread) stop() is called. * * @return true if starting worked; false otherwise */ bool start(); /** * Stop the scan * * @return true if stopping worked; false otherwi%sse */ bool stop(); /** * Is a scan currently running? * * @return true or false */ bool is_running() const; private: struct Private; std::unique_ptr<Private> priv_; }; } // namespace Mu #endif /* MU_SCANNER_HH__ */ ����������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/index/test-scanner.cc�����������������������������������������������������������������0000664�0000000�0000000�00000003262�14523230065�0017017�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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-scanner.hh" #include "mu-utils.hh" using namespace Mu; static void test_scan_maildir() { allow_warnings(); Scanner scanner{ "/home/djcb/Maildir", [](const dirent* dentry) -> bool { g_print("%02x %s\n", dentry->d_type, dentry->d_name); return true; }, [](const std::string& fullpath, const struct stat* statbuf, auto&& info) -> bool { g_print("%s %zu\n", fullpath.c_str(), statbuf->st_size); return true; }}; g_assert_true(scanner.start()); while (scanner.is_running()) { sleep(1); } } int main(int argc, char* argv[]) try { g_test_init(&argc, &argv, NULL); g_test_add_func("/utils/scanner/scan-maildir", test_scan_maildir); return g_test_run(); } catch (const std::runtime_error& re) { std::cerr << re.what() << "\n"; return 1; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/meson.build���������������������������������������������������������������������������0000664�0000000�0000000�00000004024�14523230065�0015132�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Copyright (C) 2021-2022 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. subdir('utils') subdir('message') subdir('index') lib_mu=static_library( 'mu', [ 'mu-bookmarks.cc', 'mu-contacts-cache.cc', 'mu-maildir.cc', 'mu-parser.cc', 'mu-query-match-deciders.cc', 'mu-query-threads.cc', 'mu-query.cc', 'mu-script.cc', 'mu-server.cc', 'mu-store.cc', 'mu-tokenizer.cc', 'mu-xapian.cc' ], dependencies: [ glib_dep, gio_dep, gmime_dep, xapian_dep, guile_dep, config_h_dep, lib_mu_utils_dep, lib_mu_message_dep, lib_mu_index_dep ], install: false) lib_mu_dep = declare_dependency( link_with: lib_mu, dependencies: [ lib_mu_message_dep, thread_dep ], include_directories: include_directories(['.', '..'])) # dev helpers tokenize = executable( 'tokenize', [ 'mu-tokenizer.cc', 'tokenize.cc' ], dependencies: [ lib_mu_utils_dep, glib_dep ], install: false) # actual tests test('test-threads', executable('test-threads', 'mu-query-threads.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, lib_mu_dep])) test('test-contacts-cache', executable('test-contacts-cache', 'mu-contacts-cache.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, lib_mu_dep])) subdir('tests') ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0014414�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/meson.build�������������������������������������������������������������������0000664�0000000�0000000�00000006231�14523230065�0016560�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Copyright (C) 2022 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. lib_mu_message=static_library( 'mu-message', [ 'mu-message.cc', 'mu-message-file.cc', 'mu-message-part.cc', 'mu-contact.cc', 'mu-document.cc', 'mu-fields.cc', 'mu-flags.cc', 'mu-priority.cc', 'mu-mime-object.cc', ], dependencies: [ glib_dep, gmime_dep, xapian_dep, config_h_dep, lib_mu_utils_dep], install: false) lib_mu_message_dep = declare_dependency( link_with: lib_mu_message, dependencies: [ xapian_dep, gmime_dep ], include_directories: include_directories(['.', '..'])) # # tests # test('test-contact', executable('test-contact', 'mu-contact.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) test('test-document', executable('test-document', 'mu-document.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) test('test-fields', executable('test-fields', 'mu-fields.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) test('test-flags', executable('test-flags', 'mu-flags.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) test('test-message', executable('test-message', 'test-mu-message.cc', install: false, dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) test('test-priority', executable('test-priority', 'mu-priority.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) test('test-message-file', executable('test-message-file', 'mu-message-file.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, lib_mu_message_dep])) test('test-message-part', executable('test-message-part', 'mu-message-part.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, lib_mu_message_dep])) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-contact.cc�����������������������������������������������������������������0000664�0000000�0000000�00000013633�14523230065�0017003�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022-2023 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-contact.hh" #include "mu-message.hh" #include "utils/mu-utils.hh" #include "utils/mu-regex.hh" #include "mu-mime-object.hh" #include <gmime/gmime.h> #include <glib.h> #include <string> using namespace Mu; std::string Contact::display_name() const { const auto needs_quoting= [](const std::string& n) { for (auto& c: n) if (c == ',' || c == '"' || c == '@') return true; return false; }; if (name.empty()) return email; else if (!needs_quoting(name)) return name + " <" + email + '>'; else return Mu::quote(name) + " <" + email + '>'; } static Regex email_rx; bool Contact::has_valid_email() const { /* regexp as per: * https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address * * "This requirement is a willful violation of RFC 5322, which defines a * syntax for email addresses that is simultaneously too strict (before * the "@" character), too vague (after the "@" character), and too lax * (allowing comments, whitespace characters, and quoted strings in * manners unfamiliar to most users) to be of practical use here." */ constexpr auto email_rx_str = R"(^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$)"; /* avoid std::regex here, since speed matters. PCRE is faster */ if (!email_rx) { if (auto&& res = Regex::make(email_rx_str, G_REGEX_OPTIMIZE); !res) { g_error("BUG: error in regex: %s", res.error().what()); /* terminates process */ } else email_rx = res.value(); } return email_rx.matches(email); } std::string Mu::to_string(const Mu::Contacts& contacts) { std::string res; seq_for_each(contacts, [&](auto&& contact) { if (res.empty()) res = contact.display_name(); else res += ", " + contact.display_name(); }); return res; } size_t Mu::lowercase_hash(const std::string& s) { std::size_t djb = 5381; // djb hash for (const auto c : s) djb = ((djb << 5) + djb) + static_cast<size_t>(g_ascii_tolower(c)); return djb; } #ifdef BUILD_TESTS /* * Tests. * */ #include "utils/mu-test-utils.hh" static void test_ctor_foo() { Contact c{ "foo@example.com", "Foo Bar", Contact::Type::Bcc, 1645214647 }; assert_equal(c.email, "foo@example.com"); assert_equal(c.name, "Foo Bar"); g_assert_true(c.has_valid_email()); g_assert_true(*c.field_id() == Field::Id::Bcc); g_assert_cmpuint(c.message_date,==,1645214647); assert_equal(c.display_name(), "Foo Bar <foo@example.com>"); } static void test_ctor_blinky() { Contact c{ "bar@example.com", "Blinky", 1645215014, true, /* personal */ 13, /*freq*/ 12345 /* tstamp */ }; assert_equal(c.email, "bar@example.com"); assert_equal(c.name, "Blinky"); g_assert_true(c.has_valid_email()); g_assert_true(c.personal); g_assert_cmpuint(c.frequency,==,13); g_assert_cmpuint(c.tstamp,==,12345); g_assert_cmpuint(c.message_date,==,1645215014); assert_equal(c.display_name(), "Blinky <bar@example.com>"); } static void test_ctor_cleanup() { Contact c{ "bar@example.com", "Bli\nky", 1645215014, true, /* personal */ 13, /*freq*/ 12345 /* tstamp */ }; assert_equal(c.email, "bar@example.com"); assert_equal(c.name, "Bli ky"); g_assert_true(c.personal); g_assert_true(c.has_valid_email()); g_assert_cmpuint(c.frequency,==,13); g_assert_cmpuint(c.tstamp,==,12345); g_assert_cmpuint(c.message_date,==,1645215014); assert_equal(c.display_name(), "Bli ky <bar@example.com>"); } static void test_encode() { Contact c{ "cassius@example.com", "Ali, Muhammad \"The Greatest\"", 345, false, /* personal */ 333, /*freq*/ 768 /* tstamp */ }; assert_equal(c.email, "cassius@example.com"); assert_equal(c.name, "Ali, Muhammad \"The Greatest\""); g_assert_true(c.has_valid_email()); g_assert_false(c.personal); g_assert_cmpuint(c.frequency,==,333); g_assert_cmpuint(c.tstamp,==,768); g_assert_cmpuint(c.message_date,==,345); assert_equal(c.display_name(), "\"Ali, Muhammad \\\"The Greatest\\\"\" <cassius@example.com>"); } static void test_sender() { Contact c{"aa@example.com", "Anders Ångström", Contact::Type::Sender, 54321}; assert_equal(c.email, "aa@example.com"); assert_equal(c.name, "Anders Ångström"); g_assert_true(c.has_valid_email()); g_assert_false(c.personal); g_assert_cmpuint(c.frequency,==,1); g_assert_cmpuint(c.message_date,==,54321); g_assert_false(!!c.field_id()); } static void test_misc() { g_assert_false(!!contact_type_from_field_id(Field::Id::Subject)); } static void test_broken_email() { Contact c{"a***@@booa@example..com", "abcdef", Contact::Type::Sender, 54321}; g_assert_false(c.has_valid_email()); } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_mime_init(); g_test_add_func("/message/contact/ctor-foo", test_ctor_foo); g_test_add_func("/message/contact/ctor-blinky", test_ctor_blinky); g_test_add_func("/message/contact/ctor-cleanup", test_ctor_cleanup); g_test_add_func("/message/contact/encode", test_encode); g_test_add_func("/message/contact/sender", test_sender); g_test_add_func("/message/contact/misc", test_misc); g_test_add_func("/message/contact/broken-email", test_broken_email); return g_test_run(); } #endif /*BUILD_TESTS*/ �����������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-contact.hh�����������������������������������������������������������������0000664�0000000�0000000�00000012602�14523230065�0017010�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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_MESSAGE_CONTACT_HH__ #define MU_MESSAGE_CONTACT_HH__ #include <functional> #include <string> #include <vector> #include <functional> #include <cctype> #include <cstring> #include <cstdlib> #include <ctime> #include <utils/mu-option.hh> #include "mu-fields.hh" struct _InternetAddressList; namespace Mu { /** * Get the hash value for a lowercase value of s; useful for email-addresses * * @param s a string * * @return a hash value. */ size_t lowercase_hash(const std::string& s); struct Contact { enum struct Type { None, Sender, From, ReplyTo, To, Cc, Bcc }; /** * Construct a new Contact * * @param email_ email address * @param name_ name or empty * @param type_ contact field type * @param message_date_ data for the message for this contact */ Contact(const std::string& email_, const std::string& name_ = "", Type type_ = Type::None, ::time_t message_date_ = 0) : email{email_}, name{name_}, type{type_}, message_date{message_date_}, personal{}, frequency{1}, tstamp{} { cleanup_name(); } /** * Construct a new Contact * * @param email_ email address * @param name_ name or empty * @param message_date_ date of message this contact originate from * @param personal_ is this a personal contact? * @param freq_ how often was this contact seen? * @param tstamp_ timestamp for last change */ Contact(const std::string& email_, const std::string& name_, time_t message_date_, bool personal_, size_t freq_, int64_t tstamp_) : email{email_}, name{name_}, type{Type::None}, message_date{message_date_}, personal{personal_}, frequency{freq_}, tstamp{tstamp_} { cleanup_name();} /** * Get the "display name" for this contact: * * If there's a non-empty name, it's Jane Doe <email@example.com> * otherwise it's just the e-mail address. Names with commas are quoted * (with the quotes escaped). * * @return the display name */ std::string display_name() const; /** * Does the contact contain a valid email address as per * https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address * ? * * @return true or false */ bool has_valid_email() const; /** * Operator==; based on the hash values (ie. lowercase e-mail address) * * @param rhs some other Contact * * @return true orf false. */ bool operator== (const Contact& rhs) const noexcept { return hash() == rhs.hash(); } /** * Get a hash-value for this contact, which gets lazily calculated. This * * is for use with container classes. This uses the _lowercase_ email * address. * * @return the hash */ size_t hash() const { static size_t cached_hash; if (cached_hash == 0) { cached_hash = lowercase_hash(email); } return cached_hash; } /** * Get the corresponding Field::Id (if any) * for this contact. * * @return the field-id or Nothing. */ constexpr Option<Field::Id> field_id() const noexcept { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" switch(type) { case Type::Bcc: return Field::Id::Bcc; case Type::Cc: return Field::Id::Cc; case Type::From: return Field::Id::From; case Type::To: return Field::Id::To; default: return Nothing; } #pragma GCC diagnostic pop } /* * data members */ std::string email; /**< Email address for this contact.Not empty */ std::string name; /**< Name for this contact; can be empty. */ Type type; /**< Type of contact */ int64_t message_date; /**< Date of the contact's message */ bool personal; /**< A personal message? */ size_t frequency; /**< Frequency of this contact */ int64_t tstamp; /**< Timestamp for this contact (internal use) */ private: void cleanup_name() { // replace control characters by spaces. for (auto& c: name) if (iscntrl(c)) c = ' '; } }; constexpr Option<Contact::Type> contact_type_from_field_id(Field::Id id) noexcept { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" switch(id) { case Field::Id::Bcc: return Contact::Type::Bcc; case Field::Id::Cc: return Contact::Type::Cc; case Field::Id::From: return Contact::Type::From; case Field::Id::To: return Contact::Type::To; default: return Nothing; } #pragma GCC diagnostic pop } using Contacts = std::vector<Contact>; /** * Get contacts as a comma-separated list. * * @param contacts contacs * * @return string with contacts. */ std::string to_string(const Contacts& contacts); } // namespace Mu /** * Implement our hash int std:: */ template<> struct std::hash<Mu::Contact> { std::size_t operator()(const Mu::Contact& c) const noexcept { return c.hash(); } }; #endif /* MU_CONTACT_HH__ */ ������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-document.cc����������������������������������������������������������������0000664�0000000�0000000�00000027640�14523230065�0017171�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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-document.hh" #include "mu-message.hh" #include "utils/mu-sexp.hh" #include <cstdint> #include <glib.h> #include <numeric> #include <algorithm> #include <charconv> #include <cinttypes> #include <string> #include <utils/mu-utils.hh> using namespace Mu; constexpr uint8_t SepaChar1 = 0xfe; constexpr uint8_t SepaChar2 = 0xff; const Xapian::Document& Document::xapian_document() const { if (dirty_sexp_) { xdoc_.set_data(sexp_.to_string()); dirty_sexp_ = false; } return xdoc_; } template<typename SexpType> void Document::put_prop(const std::string& pname, SexpType&& val) { sexp_.put_props(pname, std::forward<SexpType>(val)); dirty_sexp_ = true; } template<typename SexpType> void Document::put_prop(const Field& field, SexpType&& val) { put_prop(std::string(":") + std::string{field.name}, std::forward<SexpType>(val)); } static void add_search_term(Xapian::Document& doc, const Field& field, const std::string& val) { if (field.is_normal_term()) { doc.add_term(field.xapian_term(val)); } else if (field.is_boolean_term()) { doc.add_boolean_term(field.xapian_term(val)); } else if (field.is_indexable_term()) { Xapian::TermGenerator termgen; termgen.set_document(doc); termgen.index_text(utf8_flatten(val), 1, field.xapian_term()); /* also add as 'normal' term, so some queries where the indexer * eats special chars also match */ if (field.id != Field::Id::BodyText && field.id != Field::Id::EmbeddedText) { doc.add_term(field.xapian_term(val)); } } else throw std::logic_error("not a search term"); } /* hack... import html text as if it were plain text. */ static void add_body_html(Xapian::Document& doc, const Field& field, const std::string& val) { static Field body_field = field_from_id(Field::Id::BodyText); Xapian::TermGenerator termgen; termgen.set_document(doc); termgen.index_text(utf8_flatten(val), 1, body_field.xapian_term()); } void Document::add(Field::Id id, const std::string& val) { const auto field{field_from_id(id)}; if (field.is_value()) xdoc_.add_value(field.value_no(), val); if (field.is_searchable()) add_search_term(xdoc_, field, val); else if (id == Field::Id::XBodyHtml) add_body_html(xdoc_, field, val); if (field.include_in_sexp()) { put_prop(field, val); } } void Document::add(Field::Id id, const std::vector<std::string>& vals) { if (vals.empty()) return; const auto field{field_from_id(id)}; if (field.is_value()) xdoc_.add_value(field.value_no(), Mu::join(vals, SepaChar1)); if (field.is_searchable()) std::for_each(vals.begin(), vals.end(), [&](const auto& val) { add_search_term(xdoc_, field, val); }); if (field.include_in_sexp()) { Sexp elms{}; for(auto&& val: vals) elms.add(val); put_prop(field, std::move(elms)); } } std::vector<std::string> Document::string_vec_value(Field::Id field_id) const noexcept { return Mu::split(string_value(field_id), SepaChar1); } static Sexp make_contacts_sexp(const Contacts& contacts) { Sexp contacts_sexp; seq_for_each(contacts, [&](auto&& c) { Sexp contact(":email"_sym, c.email); if (!c.name.empty()) contact.add(":name"_sym, c.name); contacts_sexp.add(std::move(contact)); }); return contacts_sexp; } void Document::add(Field::Id id, const Contacts& contacts) { if (contacts.empty()) return; const auto field{field_from_id(id)}; std::vector<std::string> cvec; const std::string sepa2(1, SepaChar2); Xapian::TermGenerator termgen; termgen.set_document(xdoc_); for (auto&& contact: contacts) { const auto cfield_id{contact.field_id()}; if (!cfield_id || *cfield_id != id) continue; const auto e{contact.email}; xdoc_.add_term(field.xapian_term(e)); /* allow searching for address components, too */ const auto atpos = e.find('@'); if (atpos != std::string::npos && atpos < e.size() - 1) { xdoc_.add_term(field.xapian_term(e.substr(0, atpos))); xdoc_.add_term(field.xapian_term(e.substr(atpos + 1))); } if (!contact.name.empty()) termgen.index_text(utf8_flatten(contact.name), 1, field.xapian_term()); cvec.emplace_back(contact.email + sepa2 + contact.name); } if (!cvec.empty()) xdoc_.add_value(field.value_no(), join(cvec, SepaChar1)); if (field.include_in_sexp()) put_prop(field, make_contacts_sexp(contacts)); } Contacts Document::contacts_value(Field::Id id) const noexcept { const auto vals{string_vec_value(id)}; Contacts contacts; contacts.reserve(vals.size()); const auto ctype{contact_type_from_field_id(id)}; if (G_UNLIKELY(!ctype)) { g_critical("invalid field-id for contact-type: <%zu>", static_cast<size_t>(id)); return {}; } for (auto&& s: vals) { const auto pos = s.find(SepaChar2); if (G_UNLIKELY(pos == std::string::npos)) { g_critical("invalid contact data '%s'", s.c_str()); break; } contacts.emplace_back(s.substr(0, pos), s.substr(pos + 1), *ctype); } return contacts; } void Document::add_extra_contacts(const std::string& propname, const Contacts& contacts) { if (!contacts.empty()) { put_prop(propname, make_contacts_sexp(contacts)); dirty_sexp_ = true; } } static Sexp make_emacs_time_sexp(::time_t t) { return Sexp().add(static_cast<unsigned>(t >> 16), static_cast<unsigned>(t & 0xffff), 0); } void Document::add(Field::Id id, int64_t val) { /* * Xapian stores everything (incl. numbers) as strings. * * we comply, by storing a number a base-16 and prefixing with 'f' + * length; such that the strings are sorted in the numerical order. */ const auto field{field_from_id(id)}; if (field.is_value()) xdoc_.add_value(field.value_no(), to_lexnum(val)); if (field.include_in_sexp()) { if (field.is_time_t()) put_prop(field, make_emacs_time_sexp(val)); else put_prop(field, val); } } int64_t Document::integer_value(Field::Id field_id) const noexcept { if (auto&& v{string_value(field_id)}; v.empty()) return 0; else return from_lexnum(v); } void Document::add(Priority prio) { constexpr auto field{field_from_id(Field::Id::Priority)}; xdoc_.add_value(field.value_no(), std::string(1, to_char(prio))); xdoc_.add_boolean_term(field.xapian_term(to_char(prio))); if (field.include_in_sexp()) put_prop(field, Sexp::Symbol(priority_name(prio))); } Priority Document::priority_value() const noexcept { const auto val{string_value(Field::Id::Priority)}; return priority_from_char(val.empty() ? 'n' : val[0]); } void Document::add(Flags flags) { constexpr auto field{field_from_id(Field::Id::Flags)}; Sexp flaglist; xdoc_.add_value(field.value_no(), to_lexnum(static_cast<int64_t>(flags))); flag_infos_for_each([&](auto&& flag_info) { auto term=[&](){return field.xapian_term(flag_info.shortcut_lower());}; if (any_of(flag_info.flag & flags)) { xdoc_.add_boolean_term(term()); flaglist.add(Sexp::Symbol(flag_info.name)); } }); if (field.include_in_sexp()) put_prop(field, std::move(flaglist)); } Flags Document::flags_value() const noexcept { return static_cast<Flags>(integer_value(Field::Id::Flags)); } void Document::remove(Field::Id field_id) { const auto field{field_from_id(field_id)}; const auto pfx{field.xapian_prefix()}; xapian_try([&]{ if (auto&& val{xdoc_.get_value(field.value_no())}; !val.empty()) { // g_debug("removing value<%u>: '%s'", field.value_no(), // val.c_str()); xdoc_.remove_value(field.value_no()); } std::vector<std::string> kill_list; for (auto&& it = xdoc_.termlist_begin(); it != xdoc_.termlist_end(); ++it) { const auto term{*it}; if (!term.empty() && term.at(0) == pfx) kill_list.emplace_back(term); } for (auto&& term: kill_list) { // g_debug("removing term '%s'", term.c_str()); try { xdoc_.remove_term(term); } catch(const Xapian::InvalidArgumentError& xe) { g_critical("failed to remove '%s'", term.c_str()); } } }); } #ifdef BUILD_TESTS #include "utils/mu-test-utils.hh" #define assert_same_contact(C1,C2) do { \ g_assert_cmpstr(C1.email.c_str(),==,C2.email.c_str()); \ g_assert_cmpstr(C2.name.c_str(),==,C2.name.c_str()); \ } while (0) #define assert_same_contacts(CV1,CV2) do { \ g_assert_cmpuint(CV1.size(),==,CV2.size()); \ for (auto i = 0U; i != CV1.size(); ++i) \ assert_same_contact(CV1[i], CV2[i]); \ } while(0) static const Contacts test_contacts = {{ Contact{"john@example.com", "John", Contact::Type::Bcc}, Contact{"ringo@example.com", "Ringo", Contact::Type::Bcc}, Contact{"paul@example.com", "Paul", Contact::Type::Cc}, Contact{"george@example.com", "George", Contact::Type::Cc}, Contact{"james@example.com", "James", Contact::Type::From}, Contact{"lars@example.com", "Lars", Contact::Type::To}, Contact{"kirk@example.com", "Kirk", Contact::Type::To}, Contact{"jason@example.com", "Jason", Contact::Type::To} }}; static void test_bcc() { { Document doc; doc.add(Field::Id::Bcc, test_contacts); Contacts expected_contacts = {{ Contact{"john@example.com", "John", Contact::Type::Bcc}, Contact{"ringo@example.com", "Ringo", Contact::Type::Bcc}, }}; const auto actual_contacts = doc.contacts_value(Field::Id::Bcc); assert_same_contacts(expected_contacts, actual_contacts); } { Document doc; Contacts contacts = {{ Contact{"john@example.com", "John Lennon", Contact::Type::Bcc}, Contact{"ringo@example.com", "Ringo", Contact::Type::Bcc}, }}; doc.add(Field::Id::Bcc, contacts); TempDir tempdir; auto db = Xapian::WritableDatabase(tempdir.path()); db.add_document(doc.xapian_document()); auto contacts2 = doc.contacts_value(Field::Id::Bcc); assert_same_contacts(contacts, contacts2); } } static void test_cc() { Document doc; doc.add(Field::Id::Cc, test_contacts); Contacts expected_contacts = {{ Contact{"paul@example.com", "Paul", Contact::Type::Cc}, Contact{"george@example.com", "George", Contact::Type::Cc} }}; const auto actual_contacts = doc.contacts_value(Field::Id::Cc); assert_same_contacts(expected_contacts, actual_contacts); } static void test_from() { Document doc; doc.add(Field::Id::From, test_contacts); Contacts expected_contacts = {{ Contact{"james@example.com", "James", Contact::Type::From}, }}; const auto actual_contacts = doc.contacts_value(Field::Id::From); assert_same_contacts(expected_contacts, actual_contacts); } static void test_to() { Document doc; doc.add(Field::Id::To, test_contacts); Contacts expected_contacts = {{ Contact{"lars@example.com", "Lars", Contact::Type::To}, Contact{"kirk@example.com", "Kirk", Contact::Type::To}, Contact{"jason@example.com", "Jason", Contact::Type::To} }}; const auto actual_contacts = doc.contacts_value(Field::Id::To); assert_same_contacts(expected_contacts, actual_contacts); } static void test_size() { { Document doc; doc.add(Field::Id::Size, 12345); g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,12345); } { Document doc; g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,0); } } int main(int argc, char* argv[]) { g_test_init(&argc, &argv, NULL); g_test_add_func("/message/document/bcc", test_bcc); g_test_add_func("/message/document/cc", test_cc); g_test_add_func("/message/document/from", test_from); g_test_add_func("/message/document/to", test_to); g_test_add_func("/message/document/size", test_size); return g_test_run(); } #endif /*BUILD_TESTS*/ ������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-document.hh����������������������������������������������������������������0000664�0000000�0000000�00000012306�14523230065�0017174�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/** Copyright (C) 2022 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_DOCUMENT_HH__ #define MU_DOCUMENT_HH__ #include <xapian.h> #include <utility> #include <string> #include <vector> #include "utils/mu-xapian-utils.hh" #include "mu-fields.hh" #include "mu-priority.hh" #include "mu-flags.hh" #include "mu-contact.hh" #include <utils/mu-option.hh> #include <utils/mu-sexp.hh> namespace Mu { /** * A Document describes the information about a message that is * or can be stored in the database. * */ class Document { public: /** * Construct a message for a new Xapian Document * */ Document() {} /** * Construct a message document based on on existing Xapian document. * * @param doc */ Document(const Xapian::Document& doc): xdoc_{doc} { if (auto&& s{Sexp::parse(xdoc_.get_data())}; s) sexp_ = std::move(*s); } /** * DTOR */ ~Document() { xapian_document(); // for side-effect up updating sexp. } /** * Get a reference to the underlying Xapian document. * */ const Xapian::Document& xapian_document() const; /** * Get the doc-id for this document * * @return the docid */ Xapian::docid docid() const { return xdoc_.get_docid(); } /* * updating a document with terms & values */ /** * Add a string value to the document * * @param field_id field id * @param val string value */ void add(Field::Id field_id, const std::string& val); /** * Add a string-vec value to the document, if non-empty * * @param field_id field id * @param val string-vec value */ void add(Field::Id field_id, const std::vector<std::string>& vals); /** * Add message-contacts to the document, if non-empty * * @param field_id field id * @param contacts message contacts */ void add(Field::Id id, const Contacts& contacts); /** * Add some extra contacts with the given propname; this is useful for * ":reply-to" and ":list-post" which don't have a Field::Id and are * only present in the sexp, not in the terms/values * * @param propname property name (e.g.,. ":reply-to") * @param contacts contacts for this property. */ void add_extra_contacts(const std::string& propname, const Contacts& contacts); /** * Add an integer value to the document * * @param field_id field id * @param val integer value */ void add(Field::Id field_id, int64_t val); /** * Add a message priority to the document * * @param prio priority */ void add(Priority prio); /** * Add message flags to the document * * @param flags mesage flags. */ void add(Flags flags); /** * Remove values and terms for some field. * * @param field_id */ void remove(Field::Id field_id); /** * Get the cached s-expression useful for changing * it (call update_sexp_cache() when done) * * @return the cached s-expression */ const Sexp& sexp() const { return sexp_; } /** * Generically adds an optional value, if set, to the document * * @param id the field 0d * @param an optional value */ template<typename T> void add(Field::Id id, const Option<T>& val) { if (val) add(id, val.value()); } /* * Retrieving values */ /** * Get a message-field as a string-value * * @param field_id id of the field to get. * * @return a string (empty if not found) */ std::string string_value(Field::Id field_id) const noexcept { return xapian_try([&]{ return xdoc_.get_value(field_from_id(field_id).value_no()); }, std::string{}); } /** * Get a vec of string values. * * @param field_id id of the field to get * * @return a string list */ std::vector<std::string> string_vec_value(Field::Id field_id) const noexcept; /** * Get an integer value * * @param field_id id of the field to get * * @return an integer or 0 if not found. */ int64_t integer_value(Field::Id field_id) const noexcept; /** * Get contacts * * @param field_id id of the contacts field to get * * @return a contacts list */ Contacts contacts_value(Field::Id id) const noexcept; /** * Get the priority * * @return the message priority */ Priority priority_value() const noexcept; /** * Get the message flags * * * @return flags */ Flags flags_value() const noexcept; private: template<typename SexpType> void put_prop(const Field& field, SexpType&& val); template<typename SexpType> void put_prop(const std::string& pname, SexpType&& val); mutable Xapian::Document xdoc_; Sexp sexp_; mutable bool dirty_sexp_{}; /* xdoc's sexp is outdated */ }; } // namepace Mu #endif /* MU_DOCUMENT_HH__ */ ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-fields.cc������������������������������������������������������������������0000664�0000000�0000000�00000011346�14523230065�0016615�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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-fields.hh" #include "mu-flags.hh" #include "utils/mu-test-utils.hh" using namespace Mu; std::string Field::xapian_term(const std::string& s) const { const auto start{std::string(1U, xapian_prefix())}; if (const auto& size = s.size(); size == 0) return start; std::string res{start}; res.reserve(s.size() + 10); /* slightly optimized common pure-ascii. */ if (G_LIKELY(g_str_is_ascii(s.c_str()))) { res += s; for (auto i = 1; res[i]; ++i) res[i] = g_ascii_tolower(res[i]); } else res += utf8_flatten(s); if (G_UNLIKELY(res.size() > MaxTermLength)) res.erase(MaxTermLength); return res; } /** * compile-time checks */ constexpr bool validate_field_ids() { for (auto id = 0U; id != Field::id_size(); ++id) { const auto field_id = static_cast<Field::Id>(id); if (field_from_id(field_id).id != field_id) return false; } return true; } constexpr bool validate_field_shortcuts() { #ifdef BUILD_TESTS std::array<size_t, 26> no_dups = {0}; #endif /*BUILD_TESTS*/ for (auto id = 0U; id != Field::id_size(); ++id) { const auto field_id = static_cast<Field::Id>(id); const auto shortcut = field_from_id(field_id).shortcut; if (shortcut != 0 && (shortcut < 'a' || shortcut > 'z')) return false; #ifdef BUILD_TESTS if (shortcut != 0) { if (++no_dups[static_cast<size_t>(shortcut-'a')] > 1) { g_critical("shortcut '%c' is duplicated", shortcut); return false; } } #endif } return true; } constexpr /*static*/ bool validate_field_flags() { for (auto&& field: Fields) { /* - A field has at most one of Indexable, HasTerms, IsXapianBoolean and IsContact. */ size_t flagnum{}; if (field.is_indexable_term()) ++flagnum; if (field.is_boolean_term()) ++flagnum; if (field.is_normal_term()) ++flagnum; if (flagnum > 1) { //g_warning("invalid field %*.s", STR_V(field.name)); return false; } } return true; } /* * tests... also build as runtime-tests, so we can get coverage info */ #ifdef BUILD_TESTS #define static_assert g_assert_true #endif /*BUILD_TESTS*/ [[maybe_unused]] static void test_ids() { static_assert(validate_field_ids()); } [[maybe_unused]] static void test_shortcuts() { static_assert(validate_field_shortcuts()); } [[maybe_unused]] static void test_prefix() { static_assert(field_from_id(Field::Id::Subject).xapian_prefix() == 'S'); static_assert(field_from_id(Field::Id::XBodyHtml).xapian_prefix() == 0); } [[maybe_unused]] static void test_field_flags() { static_assert(validate_field_flags()); } #ifdef BUILD_TESTS static void test_field_from_name() { g_assert_true(field_from_name("s")->id == Field::Id::Subject); g_assert_true(field_from_name("subject")->id == Field::Id::Subject); g_assert_false(!!field_from_name("8")); g_assert_false(!!field_from_name("")); g_assert_true(field_from_name("").value_or(field_from_id(Field::Id::Bcc)).id == Field::Id::Bcc); } static void test_xapian_term() { using namespace std::string_literals; using namespace std::literals; assert_equal(field_from_id(Field::Id::Subject).xapian_term(""s), "S"); assert_equal(field_from_id(Field::Id::Subject).xapian_term("boo"s), "Sboo"); assert_equal(field_from_id(Field::Id::From).xapian_term('x'), "Fx"); assert_equal(field_from_id(Field::Id::To).xapian_term("boo"sv), "Tboo"); auto s1 = field_from_id(Field::Id::Subject).xapian_term(std::string(MaxTermLength - 1, 'x')); auto s2 = field_from_id(Field::Id::Subject).xapian_term(std::string(MaxTermLength, 'x')); g_assert_cmpuint(s1.length(), ==, s2.length()); } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_test_add_func("/message/fields/ids", test_ids); g_test_add_func("/message/fields/shortcuts", test_shortcuts); g_test_add_func("/message/fields/from-name", test_field_from_name); g_test_add_func("/message/fields/prefix", test_prefix); g_test_add_func("/message/fields/xapian-term", test_xapian_term); g_test_add_func("/message/fields/flags", test_field_flags); return g_test_run(); } #endif /*BUILD_TESTS*/ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-fields.hh������������������������������������������������������������������0000664�0000000�0000000�00000031320�14523230065�0016621�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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_FIELDS_HH__ #define MU_FIELDS_HH__ #include <cstdint> #include <string_view> #include <algorithm> #include <array> #include <xapian.h> #include <utils/mu-utils.hh> #include <utils/mu-option.hh> namespace Mu { // Xapian does not like terms much longer than this constexpr auto MaxTermLength = 240; // http://article.gmane.org/gmane.comp.search.xapian.general/3656 */ struct Field { /** * Field Ids. * * Note, the Ids are also used as indices in the Fields array, * so their numerical values must be 0...Count. * */ enum struct Id { Bcc = 0, /**< Blind Carbon-Copy */ BodyText, /**< Text body */ Cc, /**< Carbon-Copy */ Changed, /**< Last change time (think 'ctime') */ Date, /**< Message date */ EmbeddedText, /**< Embedded text in message */ File, /**< Filename */ Flags, /**< Message flags */ From, /**< Message sender */ Maildir, /**< Maildir path */ MailingList, /**< Mailing list */ MessageId, /**< Message Id */ MimeType, /**< MIME-Type */ Path, /**< File-system Path */ Priority, /**< Message priority */ References, /**< All references (incl. Reply-To:) */ Size, /**< Message size (in bytes) */ Subject, /**< Message subject */ Tags, /**< Message Tags */ ThreadId, /**< Thread Id */ To, /**< To: recipient */ /* * <private> */ XBodyHtml, /**< HTML Body */ _count_ /**< Number of FieldIds */ }; /** * Get the number of Id values. * * @return the number. */ static constexpr size_t id_size() { return static_cast<size_t>(Id::_count_); } constexpr Xapian::valueno value_no() const { return static_cast<Xapian::valueno>(id); } /** * Field types * */ enum struct Type { String, /**< String */ StringList, /**< List of strings */ ContactList, /**< List of contacts */ ByteSize, /**< Size in bytes */ TimeT, /**< A time_t value */ Integer, /**< An integer */ }; constexpr bool is_string() const { return type == Type::String; } constexpr bool is_string_list() const { return type == Type::StringList; } constexpr bool is_byte_size() const { return type == Type::ByteSize; } constexpr bool is_time_t() const { return type == Type::TimeT; } constexpr bool is_integer() const { return type == Type::Integer; } constexpr bool is_numerical() const { return is_byte_size() || is_time_t() || is_integer(); } /** * Field flags * 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) * * Rules (build-time enforced): * - A field has at most one of Indexable, HasTerms, IsXapianBoolean and IsContact. */ enum struct Flag { /* * Different kind of terms; at most one is true, * and cannot be combined with IsContact. Compile-time enforced. */ NormalTerm = 1 << 0, /**< Field is a searchable term */ BooleanTerm = 1 << 1, /**< Field is a boolean search-term (i.e. at most one per message); * wildcards do not work */ IndexableTerm = 1 << 2, /**< Field has indexable text as term */ /* * Contact flag cannot be combined with any of the term flags. * This is compile-time enforced. */ Contact = 1 << 10, /**< field contains one or more e-mail-addresses */ Value = 1 << 11, /**< Field value is stored (so the literal value can be retrieved) */ Range = 1 << 21, IncludeInSexp = 1 << 24, /**< whether to include this field in the cached sexp. */ /**< whether this is a range field (e.g., date, size)*/ Internal = 1 << 26 }; constexpr bool any_of(Flag some_flag) const{ return (static_cast<int>(some_flag) & static_cast<int>(flags)) != 0; } constexpr bool is_indexable_term() const { return any_of(Flag::IndexableTerm); } constexpr bool is_boolean_term() const { return any_of(Flag::BooleanTerm); } constexpr bool is_normal_term() const { return any_of(Flag::NormalTerm); } constexpr bool is_searchable() const { return is_indexable_term() || is_boolean_term() || is_normal_term(); } constexpr bool is_sortable() const { return is_value(); } constexpr bool is_value() const { return any_of(Flag::Value); } constexpr bool is_internal() const { return any_of(Flag::Internal); } constexpr bool is_contact() const { return any_of(Flag::Contact); } constexpr bool is_range() const { return any_of(Flag::Range); } constexpr bool include_in_sexp() const { return any_of(Flag::IncludeInSexp);} /** * Field members * */ Id id; /**< Id of the message field */ Type type; /**< Type of the message field */ std::string_view name; /**< Name of the message field */ std::string_view alias; /**< Alternative name for the message field */ std::string_view description; /**< Decription of the message field */ std::string_view example_query; /**< Example query */ char shortcut; /**< Shortcut for the message field; a..z */ Flag flags; /**< Flags */ /** * Convenience / helpers * */ constexpr char xapian_prefix() const { /* xapian uses uppercase shortcuts; toupper is not constexpr */ return shortcut == 0 ? 0 : shortcut - ('a' - 'A'); } /** * Get the xapian term; truncated to MaxTermLength and * utf8-flattened. * * @param s * * @return the xapian term */ std::string xapian_term(const std::string& s="") const; std::string xapian_term(std::string_view sv) const { return xapian_term(std::string{sv}); } std::string xapian_term(char c) const { return xapian_term(std::string(1, c)); } }; MU_ENABLE_BITOPS(Field::Flag); /** * Sequence of _all_ message fields */ static constexpr std::array<Field, Field::id_size()> Fields = { { { Field::Id::Bcc, Field::Type::ContactList, "bcc", {}, "Blind carbon-copy recipient", "bcc:foo@example.com", 'h', Field::Flag::Contact | Field::Flag::Value | Field::Flag::IncludeInSexp | Field::Flag::IndexableTerm, }, { Field::Id::BodyText, Field::Type::String, "body", {}, "Message plain-text body", "body:capybara", 'b', Field::Flag::IndexableTerm, }, { Field::Id::Cc, Field::Type::ContactList, "cc", {}, "Carbon-copy recipient", "cc:quinn@example.com", 'c', Field::Flag::Contact | Field::Flag::Value | Field::Flag::IncludeInSexp | Field::Flag::IndexableTerm, }, { Field::Id::Changed, Field::Type::TimeT, "changed", {}, "Last change time", "changed:30M..", 'k', Field::Flag::Value | Field::Flag::Range | Field::Flag::IncludeInSexp }, { Field::Id::Date, Field::Type::TimeT, "date", {}, "Message date", "date:20220101..20220505", 'd', Field::Flag::Value | Field::Flag::Range | Field::Flag::IncludeInSexp }, { Field::Id::EmbeddedText, Field::Type::String, "embed", {}, "Embedded text", "embed:war OR embed:peace", 'e', Field::Flag::IndexableTerm }, { Field::Id::File, Field::Type::String, "file", {}, "Attachment file name", "file:/image\\.*.jpg/", 'j', Field::Flag::BooleanTerm }, { Field::Id::Flags, Field::Type::Integer, "flags", "flag", "Message properties", "flag:unread AND flag:personal", 'g', Field::Flag::BooleanTerm | Field::Flag::Value | Field::Flag::IncludeInSexp }, { Field::Id::From, Field::Type::ContactList, "from", {}, "Message sender", "from:jimbo", 'f', Field::Flag::Contact | Field::Flag::Value | Field::Flag::IncludeInSexp | Field::Flag::IndexableTerm, }, { Field::Id::Maildir, Field::Type::String, "maildir", {}, "Maildir path for message", "maildir:/private/archive", 'm', Field::Flag::BooleanTerm | Field::Flag::Value | Field::Flag::IncludeInSexp }, { Field::Id::MailingList, Field::Type::String, "list", {}, "Mailing list (List-Id:)", "list:mu-discuss.example.com", 'v', Field::Flag::BooleanTerm | Field::Flag::Value | Field::Flag::IncludeInSexp }, { Field::Id::MessageId, Field::Type::String, "message-id", "msgid", "Message-Id", "msgid:abc@123", 'i', Field::Flag::BooleanTerm | Field::Flag::Value | Field::Flag::IncludeInSexp }, { Field::Id::MimeType, Field::Type::String, "mime", "mime-type", "Attachment MIME-type", "mime:image/jpeg", 'y', Field::Flag::BooleanTerm }, { Field::Id::Path, Field::Type::String, "path", {}, "File system path to message", "path:/a/b/Maildir/cur/msg:2,S", 'l', Field::Flag::BooleanTerm | Field::Flag::Value | Field::Flag::IncludeInSexp }, { Field::Id::Priority, Field::Type::Integer, "priority", "prio", "Priority", "prio:high", 'p', Field::Flag::BooleanTerm | Field::Flag::Value | Field::Flag::IncludeInSexp }, { Field::Id::References, Field::Type::StringList, "references", {}, "References to related messages", {}, 'r', Field::Flag::Value | Field::Flag::IncludeInSexp }, { Field::Id::Size, Field::Type::ByteSize, "size", {}, "Message size in bytes", "size:1M..5M", 'z', Field::Flag::Value | Field::Flag::Range | Field::Flag::IncludeInSexp }, { Field::Id::Subject, Field::Type::String, "subject", {}, "Message subject", "subject:wombat", 's', Field::Flag::Value | Field::Flag::IndexableTerm | Field::Flag::IncludeInSexp }, { Field::Id::Tags, Field::Type::StringList, "tags", "tag", "Message tags", "tag:projectx", 'x', Field::Flag::BooleanTerm | Field::Flag::Value | Field::Flag::IncludeInSexp }, { Field::Id::ThreadId, Field::Type::String, "thread", {}, "Thread a message belongs to", {}, 'w', Field::Flag::BooleanTerm | Field::Flag::Value }, { Field::Id::To, Field::Type::ContactList, "to", {}, "Message recipient", "to:flimflam@example.com", 't', Field::Flag::Contact | Field::Flag::Value | Field::Flag::IncludeInSexp | Field::Flag::IndexableTerm, }, /* internal */ { Field::Id::XBodyHtml, Field::Type::String, "htmlbody", {}, "Message html body", {}, {}, Field::Flag::Internal }, }}; /* * Convenience */ /** * Get the message field for the given Id. * * @param id of the message field * * @return ref of the message field. */ constexpr const Field& field_from_id(Field::Id id) { return Fields.at(static_cast<size_t>(id)); } /** * Invoke func for each message-field * * @param func some callable */ template <typename Func> constexpr void field_for_each(Func&& func) { for (const auto& field: Fields) func(field); } /** * Find a message field that satisfies some predicate * * @param pred the predicate (a callable) * * @return a message-field id, or nullopt if not found. */ template <typename Pred> constexpr Option<Field> field_find_if(Pred&& pred) { for (auto&& field: Fields) if (pred(field)) return field; return Nothing; } /** * Get the the message-field id for the given name or shortcut * * @param name_or_shortcut * * @return the message-field-id or nullopt. */ static inline Option<Field> field_from_shortcut(char shortcut) { return field_find_if([&](auto&& field){ return field.shortcut == shortcut; }); } static inline Option<Field> field_from_name(const std::string& name) { switch(name.length()) { case 0: return Nothing; case 1: return field_from_shortcut(name[0]); default: return field_find_if([&](auto&& field){ return name == field.name || name == field.alias; }); } } /** * Get the Field::Id for some number, or nullopt if it does not match * * @param id an id number * * @return Field::Id or nullopt */ static inline Option<Field> field_from_number(size_t id) { if (id >= static_cast<size_t>(Field::Id::_count_)) return Nothing; else return field_from_id(static_cast<Field::Id>(id)); } } // namespace Mu #endif /* MU_FIELDS_HH__ */ ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-flags.cc�������������������������������������������������������������������0000664�0000000�0000000�00000012717�14523230065�0016446�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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. ** */ /* * implementation is almost completely in the header; here we just add some * compile-time tests. */ #include "mu-flags.hh" using namespace Mu; std::string Mu::to_string(Flags flags) { std::string str; for (auto&& info: AllMessageFlagInfos) if (any_of(info.flag & flags)) str+=info.shortcut; return str; } /* * flags & flag-info */ constexpr bool validate_message_info_flags() { for (auto id = 0U; id != AllMessageFlagInfos.size(); ++id) { const auto flag = static_cast<Flags>(1 << id); if (flag != AllMessageFlagInfos[id].flag) return false; } return true; } /* * tests... also build as runtime-tests, so we can get coverage info */ #ifdef BUILD_TESTS #define static_assert g_assert_true #endif /*BUILD_TESTS*/ [[maybe_unused]] static void test_basic() { static_assert(AllMessageFlagInfos.size() == __builtin_ctz(static_cast<unsigned>(Flags::_final_))); static_assert(validate_message_info_flags()); static_assert(!!flag_info(Flags::Encrypted)); static_assert(!flag_info(Flags::None)); static_assert(!flag_info(static_cast<Flags>(0))); static_assert(!flag_info(static_cast<Flags>(1<<AllMessageFlagInfos.size()))); } /* * flag_info */ [[maybe_unused]] static void test_flag_info() { static_assert(flag_info('D')->flag == Flags::Draft); static_assert(flag_info('l')->flag == Flags::MailingList); static_assert(!flag_info('y')); static_assert(flag_info("trashed")->flag == Flags::Trashed); static_assert(flag_info("attach")->flag == Flags::HasAttachment); static_assert(!flag_info("fnorb")); static_assert(flag_info('D')->shortcut_lower() == 'd'); static_assert(flag_info('u')->shortcut_lower() == 'u'); } /* * flags_from_expr */ [[maybe_unused]] static void test_flags_from_expr() { static_assert(flags_from_absolute_expr("SRP").value() == (Flags::Seen | Flags::Replied | Flags::Passed)); static_assert(flags_from_absolute_expr("Faul").value() == (Flags::Flagged | Flags::Unread | Flags::HasAttachment | Flags::MailingList)); /* note: unread is a special flag, _implied_ from "new or not seen" */ static_assert(flags_from_absolute_expr("N").value() == (Flags::New|Flags::Unread)); static_assert(!flags_from_absolute_expr("DRT?")); static_assert(flags_from_absolute_expr("DRT?", true/*ignore invalid*/).value() == (Flags::Draft | Flags::Replied | Flags::Trashed | Flags::Unread)); static_assert(flags_from_absolute_expr("DFPNxulabcdef", true/*ignore invalid*/).value() == (Flags::Draft|Flags::Flagged|Flags::Passed| Flags::New | Flags::Encrypted | Flags::Unread | Flags::MailingList | Flags::Calendar | Flags::HasAttachment)); } /* * flags_from_delta_expr */ [[maybe_unused]] static void test_flags_from_delta_expr() { static_assert(flags_from_delta_expr( "+S-u-N", Flags::New|Flags::Unread).value() == Flags::Seen); /* note: unread is a special flag, _implied_ from "new or not seen" */ static_assert(flags_from_delta_expr( "+S-N", Flags::New|Flags::Unread).value() == Flags::Seen); static_assert(flags_from_delta_expr( "-S", Flags::Seen).value() == Flags::Unread); static_assert(flags_from_delta_expr("+R+P-F", Flags::Seen).value() == (Flags::Seen|Flags::Passed|Flags::Replied)); /* '-B' is invalid */ static_assert(!flags_from_delta_expr("+R+P-B", Flags::Seen)); /* '-B' is invalid, but ignore invalid */ static_assert(flags_from_delta_expr("+R+P-B", Flags::Seen, true) == (Flags::Replied|Flags::Passed|Flags::Seen)); static_assert(flags_from_delta_expr("+F+T-S", Flags::None, true).value() == (Flags::Flagged|Flags::Trashed|Flags::Unread)); } /* * flags_filter */ [[maybe_unused]] static void test_flags_filter() { static_assert(flags_filter(flags_from_absolute_expr( "DFPNxulabcdef", true/*ignore invalid*/).value(), MessageFlagCategory::Mailfile) == (Flags::Draft|Flags::Flagged|Flags::Passed)); } [[maybe_unused]] static void test_flags_keep_unmutable() { static_assert(flags_keep_unmutable((Flags::Seen|Flags::Passed), (Flags::Flagged|Flags::Draft), Flags::Replied) == (Flags::Flagged|Flags::Draft)); } #ifdef BUILD_TESTS int main(int argc, char* argv[]) { g_test_init(&argc, &argv, NULL); g_test_add_func("/message/flags/basic", test_basic); g_test_add_func("/message/flags/flag-info", test_flag_info); g_test_add_func("/message/flags/flags-from-absolute-expr", test_flags_from_expr); g_test_add_func("/message/flags/flags-from-delta-expr", test_flags_from_delta_expr); g_test_add_func("/message/flags/flags-filter", test_flags_filter); g_test_add_func("/message/flags/flags-keep-unmutable", test_flags_keep_unmutable); return g_test_run(); } #endif /*BUILD_TESTS*/ �������������������������������������������������mu-1.10.8/lib/message/mu-flags.hh�������������������������������������������������������������������0000664�0000000�0000000�00000023657�14523230065�0016465�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022-2023 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_HH__ #define MU_FLAGS_HH__ #include <algorithm> #include <string_view> #include <array> #include <utils/mu-utils.hh> #include <utils/mu-option.hh> namespace Mu { enum struct Flags { None = 0, /**< No flags */ /** * 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) */ Draft = 1 << 0, /**< A draft message */ Flagged = 1 << 1, /**< A flagged message */ Passed = 1 << 2, /**< A passed (forwarded) message */ Replied = 1 << 3, /**< A replied message */ Seen = 1 << 4, /**< A seen (read) message */ Trashed = 1 << 5, /**< A trashed message */ /** * decides on cur/ or new/ in the maildir */ New = 1 << 6, /**< A new message */ /** * content flags -- not visible in the filename, but used for * searching */ Signed = 1 << 7, /**< Cryptographically signed */ Encrypted = 1 << 8, /**< Encrypted */ HasAttachment = 1 << 9, /**< Has an attachment */ Unread = 1 << 10, /**< Unread; pseudo-flag, only for queries, so we can * search for flag:unread, which is equivalent to * 'flag:new OR NOT flag:seen' */ /** * other content flags */ MailingList = 1 << 11, /**< A mailing-list message */ Personal = 1 << 12, /**< A personal message (i.e., at least one of the * contact fields contains a personal address) */ Calendar = 1 << 13, /**< A calendar invitation */ /* * <private> */ _final_ = 1 << 14 }; MU_ENABLE_BITOPS(Flags); /** * Message flags category * */ enum struct MessageFlagCategory { None, /**< Nothing */ Mailfile, /**< Flag for a message file */ Maildir, /**< Flag for message file's location */ Content, /**< Message content flag */ Pseudo /**< Pseudo flag */ }; /** * Info about invidual message flags * */ struct MessageFlagInfo { Flags flag; /**< The message flag */ char shortcut; /**< Shortcut character; * tolower(shortcut) must be * unique for all flags */ std::string_view name; /**< Name of the flag */ MessageFlagCategory category; /**< Flag category */ std::string_view description; /**< Description */ /** * Get the lower-case version of shortcut * * @return lower-case shortcut */ constexpr char shortcut_lower() const { return shortcut >= 'A' && shortcut <= 'Z' ? shortcut + ('a' - 'A') : shortcut; } }; /** * Array of all flag information. */ constexpr std::array<MessageFlagInfo, 14> AllMessageFlagInfos = {{ MessageFlagInfo{Flags::Draft, 'D', "draft", MessageFlagCategory::Mailfile, "Draft (in progress)" }, MessageFlagInfo{Flags::Flagged, 'F', "flagged", MessageFlagCategory::Mailfile, "User-flagged" }, MessageFlagInfo{Flags::Passed, 'P', "passed", MessageFlagCategory::Mailfile, "Forwarded message" }, MessageFlagInfo{Flags::Replied, 'R', "replied", MessageFlagCategory::Mailfile, "Replied-to" }, MessageFlagInfo{Flags::Seen, 'S', "seen", MessageFlagCategory::Mailfile, "Viewed at least once" }, MessageFlagInfo{Flags::Trashed, 'T', "trashed", MessageFlagCategory::Mailfile, "Marked for deletion" }, MessageFlagInfo{Flags::New, 'N', "new", MessageFlagCategory::Maildir, "New message" }, MessageFlagInfo{Flags::Signed, 'z', "signed", MessageFlagCategory::Content, "Cryptographically signed" }, MessageFlagInfo{Flags::Encrypted, 'x', "encrypted", MessageFlagCategory::Content, "Encrypted" }, MessageFlagInfo{Flags::HasAttachment,'a', "attach", MessageFlagCategory::Content, "Has at least one attachment" }, MessageFlagInfo{Flags::Unread, 'u', "unread", MessageFlagCategory::Pseudo, "New or not seen message" }, MessageFlagInfo{Flags::MailingList, 'l', "list", MessageFlagCategory::Content, "Mailing list message" }, MessageFlagInfo{Flags::Personal, 'q', "personal", MessageFlagCategory::Content, "Personal message" }, MessageFlagInfo{Flags::Calendar, 'c', "calendar", MessageFlagCategory::Content, "Calendar invitation" }, }}; /** * Invoke some callable Func for each flag info * * @param func some callable */ template<typename Func> constexpr void flag_infos_for_each(Func&& func) { for (auto&& info: AllMessageFlagInfos) func(info); } /** * Get flag info for some flag * * @param flag a singular flag * * @return the MessageFlagInfo, or Nothing in case of error. */ constexpr const Option<MessageFlagInfo> flag_info(Flags flag) { constexpr auto upper = static_cast<unsigned>(Flags::_final_); const auto val = static_cast<unsigned>(flag); if (__builtin_popcount(val) != 1 || val >= upper) return Nothing; return AllMessageFlagInfos[static_cast<unsigned>(__builtin_ctz(val))]; } /** * Get flag info for some flag * * @param shortcut shortcut character * * @return the MessageFlagInfo */ constexpr const Option<MessageFlagInfo> flag_info(char shortcut) { for (auto&& info : AllMessageFlagInfos) if (info.shortcut == shortcut) return info; return Nothing; } /** * Get flag info for some flag * * @param name of the message-flag. * * @return the MessageFlagInfo */ constexpr const Option<MessageFlagInfo> flag_info(std::string_view name) { for (auto&& info : AllMessageFlagInfos) if (info.name == name) return info; return Nothing; } /** * 'unread' is a pseudo-flag that means 'new or not seen' * * @param flags * * @return flags with unread added or removed. */ constexpr Flags imply_unread(Flags flags) { /* unread is a pseudo flag equivalent to 'new or not seen' */ if (any_of(flags & Flags::New) || none_of(flags & Flags::Seen)) return flags | Flags::Unread; else return flags & ~Flags::Unread; } /** * There are two string-based expression types for flags: * 1) 'absolute': replace the existing flags * 2) 'delta' : flags as a delta of existing flags. */ /** * Get the (OR'ed) flags corresponding to an expression. * * @param expr the expression (a sequence of flag shortcut characters) * @param ignore_invalid if @true, ignore invalid flags, otherwise return * nullopt if an invalid flag is encountered * * @return the (OR'ed) flags or Flags::None */ constexpr Option<Flags> flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = false) { Flags flags{Flags::None}; for (auto&& kar : expr) { if (const auto& info{flag_info(kar)}; !info) { if (!ignore_invalid) return Nothing; } else flags |= info->flag; } return imply_unread(flags); } /** * Calculate flags from existing flags and a delta expression * * Update @p flags with the flags in @p expr, where @p exprt consists of the the * normal flag shortcut characters, prefixed with either '+' or '-', which means * resp. "add this flag" or "remove this flag". * * So, e.g. "-N+S" would unset the NEW flag and set the SEEN flag, without * affecting other flags. * * @param expr delta expression * @param flags existing flags * @param ignore_invalid if @true, ignore invalid flags, otherwise return * nullopt if an invalid flag is encountered * * @return new flags, or nullopt in case of error */ constexpr Option<Flags> flags_from_delta_expr(std::string_view expr, Flags flags, bool ignore_invalid = false) { if (expr.size() % 2 != 0) return Nothing; for (auto u = 0U; u != expr.size(); u += 2) { if (const auto& info{flag_info(expr[u + 1])}; !info) { if (!ignore_invalid) return Nothing; } else { switch (expr[u]) { case '+': flags |= info->flag; break; case '-': flags &= ~info->flag; break; default: if (!ignore_invalid) return Nothing; break; } } } return imply_unread(flags); } /** * Calculate the flags from either 'absolute' or 'delta' expressions * * @param expr a flag expression, either 'delta' or 'absolute' * @param flags optional: existing flags or none. Required for delta. * * @return either messages flags or Nothing in case of error. */ constexpr Option<Flags> flags_from_expr(std::string_view expr, Option<Flags> flags = Nothing) { if (expr.empty()) return Nothing; if (expr[0] == '+' || expr[0] == '-') return flags_from_delta_expr( expr, flags.value_or(Flags::None), true); else return flags_from_absolute_expr(expr, true); } /** * Filter out flags which are not in the given category * * @param flags flags * @param cat category * * @return filter flags */ constexpr Flags flags_filter(Flags flags, MessageFlagCategory cat) { for (auto&& info : AllMessageFlagInfos) if (info.category != cat) flags &= ~info.flag; return flags; } /** * Return flags, where flags = new_flags but with unmutable_flag in the * result the same as in old_flags * * @param old_flags * @param new_flags * @param unmutable_flag * * @return */ constexpr Flags flags_keep_unmutable(Flags old_flags, Flags new_flags, Flags unmutable_flag) { if (any_of(old_flags & unmutable_flag)) return new_flags | unmutable_flag; else return new_flags & ~unmutable_flag; } /** * Get a string representation of flags * * @param flags flags * * @return string as a sequence of message-flag shortcuts */ std::string to_string(Flags flags); } // namespace Mu #endif /* MU_FLAGS_HH__ */ ���������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-message-file.cc������������������������������������������������������������0000664�0000000�0000000�00000013331�14523230065�0017704�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb.bulk@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. ** */ #include "mu-message-file.hh" using namespace Mu; Result<std::string> Mu::maildir_from_path(const std::string& path, const std::string& root) { const auto pos = path.find(root); if (pos != 0 || path[root.length()] != '/') return Err(Error{Error::Code::InvalidArgument, "root '%s' is not a root for 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) return Err(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))) return Err(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 Ok(std::move(mdir)); } Mu::FileParts Mu::message_file_parts(const std::string& file) { const auto pos{file.find_last_of(":!;")}; /* no suffix at all? */ if (pos == std::string::npos || pos > file.length() - 3 || file[pos + 1] != '2' || file[pos + 2] != ',') return FileParts{ file, ':', {}}; return FileParts { file.substr(0, pos), file[pos], file.substr(pos + 3) }; } Mu::Result<DirFile> Mu::base_message_dir_file(const std::string& path) { constexpr auto newdir{"/new"}; char *dirname{g_path_get_dirname(path.c_str())}; bool is_new{!!g_str_has_suffix(dirname, newdir)}; std::string mdir{dirname, ::strlen(dirname) - 4}; g_free(dirname); char *basename{g_path_get_basename(path.c_str())}; std::string bname{basename}; g_free(basename); return Ok(DirFile{std::move(mdir), std::move(bname), is_new}); } Mu::Result<Mu::Flags> Mu::flags_from_path(const std::string& path) { /* * this gets us the source maildir filesystem path, the directory * in which new/ & cur/ lives, and the source file */ auto dirfile{base_message_dir_file(path)}; if (!dirfile) return Err(std::move(dirfile.error())); /* a message under new/ is just.. New. Filename is not considered */ if (dirfile->is_new) return Ok(Flags::New); /* it's cur/ message, so parse the file name */ const auto parts{message_file_parts(dirfile->file)}; auto flags{flags_from_absolute_expr(parts.flags_suffix, true/*ignore invalid*/)}; if (!flags) { /* LCOV_EXCL_START*/ return Err(Error{Error::Code::InvalidArgument, "invalid flags ('%s')", parts.flags_suffix.c_str()}); /* LCOV_EXCL_STOP*/ } /* of course, only _file_ flags are allowed */ return Ok(flags_filter(flags.value(), MessageFlagCategory::Mailfile)); } #ifdef BUILD_TESTS #include "utils/mu-test-utils.hh" static void test_maildir_from_path() { std::array<std::tuple<std::string, std::string, std::string>, 1> test_cases = {{ { "/home/foo/Maildir/hello/cur/msg123", "/home/foo/Maildir", "/hello" } }}; for(auto&& tcase: test_cases) { const auto res{maildir_from_path(std::get<0>(tcase), std::get<1>(tcase))}; assert_valid_result(res); assert_equal(*res, std::get<2>(tcase)); } g_assert_false(!!maildir_from_path("/home/foo/Maildir/cur/test1", "/home/bar")); g_assert_false(!!maildir_from_path("/x", "/x/y")); g_assert_false(!!maildir_from_path("/home/a/Maildir/b/xxx/test", "/home/a/Maildir")); } static void test_base_message_dir_file() { struct TestCase { const std::string path; DirFile expected; }; std::array<TestCase, 1> test_cases = {{ { "/home/djcb/Maildir/foo/cur/msg:2,S", { "/home/djcb/Maildir/foo", "msg:2,S", false } } }}; for(auto&& tcase: test_cases) { const auto res{base_message_dir_file(tcase.path)}; assert_valid_result(res); assert_equal(res->dir, tcase.expected.dir); assert_equal(res->file, tcase.expected.file); g_assert_cmpuint(res->is_new, ==, tcase.expected.is_new); } } static void test_flags_from_path() { std::array<std::pair<std::string, Flags>, 5> test_cases = {{ {"/home/foo/Maildir/test/cur/123456:2,FSR", (Flags::Replied | Flags::Seen | Flags::Flagged)}, {"/home/foo/Maildir/test/new/123456", Flags::New}, {/* NOTE: when in new/, the :2,.. stuff is ignored */ "/home/foo/Maildir/test/new/123456:2,FR", Flags::New}, {"/home/foo/Maildir/test/cur/123456:2,DTP", (Flags::Draft | Flags::Trashed | Flags::Passed)}, {"/home/foo/Maildir/test/cur/123456:2,S", Flags::Seen} }}; for (auto&& tcase: test_cases) { auto res{flags_from_path(tcase.first)}; assert_valid_result(res); /* LCOV_EXCL_START*/ if (g_test_verbose()) { g_print("%s -> <%s>\n", tcase.first.c_str(), to_string(res.value()).c_str()); g_assert_true(res.value() == tcase.second); } /*LCOV_EXCL_STOP*/ } } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_test_add_func("/message/file/maildir-from-path", test_maildir_from_path); g_test_add_func("/message/file/base-message-dir-file", test_base_message_dir_file); g_test_add_func("/message/file/flags-from-path", test_flags_from_path); return g_test_run(); } #endif /*BUILD_TESTS*/ �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-message-file.hh������������������������������������������������������������0000664�0000000�0000000�00000005153�14523230065�0017721�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb.bulk@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. ** */ #ifndef MU_MESSAGE_FILE_HH__ #define MU_MESSAGE_FILE_HH__ #include "mu-flags.hh" #include <utils/mu-result.hh> namespace Mu { /* * The file-components, ie. * 1631819685.fb7b279bbb0a7b66.evergrey:2,RS * => { * "1631819685.fb7b279bbb0a7b66.evergrey", * ':', * "2,", * "RS" * } */ struct FileParts { std::string base; /**< basename */ char separator; /**< separator */ std::string flags_suffix; /**< suffix (with flags) */ }; /** * Get the file-parts for some message-file * * @param file path to some message file (does not have to exist) * * @return FileParts for the message file */ FileParts message_file_parts(const std::string& file); struct DirFile { std::string dir; std::string file; bool is_new; }; /** * Get information about the message file componemts * * @param path message path * * @return the components for the message file or an error. */ Result<DirFile> base_message_dir_file(const std::string& 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 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 message flags or an error */ Result<Flags> flags_from_path(const std::string& pathname); /** * get the maildir for a certain message path, ie, the path *before* * cur/ or new/ and *after* the root. * * @param path path for some message * @param root filesystem root for the maildir * * @return the maildir or an Error */ Result<std::string> maildir_from_path(const std::string& path, const std::string& root); } // Mu #endif /* MU_MESSAGE_FILE_HH__ */ ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-message-part.cc������������������������������������������������������������0000664�0000000�0000000�00000013657�14523230065�0017746�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2023 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-message-part.hh" #include "glibconfig.h" #include "mu-mime-object.hh" #include "utils/mu-utils.hh" #include <string> using namespace Mu; MessagePart::MessagePart(const Mu::MimeObject& obj): mime_obj{std::make_unique<Mu::MimeObject>(obj)} {} MessagePart::MessagePart(const MessagePart& other): MessagePart(*other.mime_obj) {} MessagePart::~MessagePart() = default; const MimeObject& MessagePart::mime_object() const noexcept { return *mime_obj; } static std::string cook(const std::string& fname, const std::vector<char>& forbidden) { std::string clean; clean.reserve(fname.length()); for (auto& c: to_string_gchar(g_path_get_basename(fname.c_str()))) if (seq_some(forbidden,[&](char fc){return ::iscntrl(c) || c == fc;})) clean += '-'; else clean += c; if (clean[0] == '.' && (clean == "." || clean == "..")) return "-"; else return clean; } static std::string cook_minimal(const std::string& fname) { return cook(fname, { '/' }); } static std::string cook_full(const std::string& fname) { auto cooked = cook(fname, { '/', ' ', '\\', ':' }); if (cooked.size() > 1 && cooked[0] == '-') cooked.erase(0, 1); return cooked; } Option<std::string> MessagePart::cooked_filename(bool minimal) const noexcept { auto&& cooker{minimal ? cook_minimal : cook_full}; // a MimePart... use the name if there is one. if (mime_object().is_part()) return MimePart{mime_object()}.filename().map(cooker); // MimeMessagepart. Construct a name based on subject. if (mime_object().is_message_part()) { auto msg{MimeMessagePart{mime_object()}.get_message()}; if (!msg) return Nothing; else return msg->subject() .map(cooker) .value_or("no-subject") + ".eml"; } return Nothing; } Option<std::string> MessagePart::raw_filename() const noexcept { if (!mime_object().is_part()) return Nothing; else return MimePart{mime_object()}.filename(); } Option<std::string> MessagePart::mime_type() const noexcept { if (const auto ctype{mime_object().content_type()}; ctype) return ctype->media_type() + "/" + ctype->media_subtype(); else return Nothing; } Option<std::string> MessagePart::content_description() const noexcept { if (!mime_object().is_part()) return Nothing; else return MimePart{mime_object()}.content_description(); } size_t MessagePart::size() const noexcept { if (!mime_object().is_part()) return 0; else return MimePart{mime_object()}.size(); } bool MessagePart::is_attachment() const noexcept { if (!mime_object().is_part()) return false; else return MimePart{mime_object()}.is_attachment(); } Option<std::string> MessagePart::to_string() const noexcept { if (mime_object().is_part()) return MimePart{mime_object()}.to_string(); else return mime_object().to_string_opt(); } Result<size_t> MessagePart::to_file(const std::string& path, bool overwrite) const noexcept { if (mime_object().is_part()) return MimePart{mime_object()}.to_file(path, overwrite); else if (mime_object().is_message_part()) { if (auto&& msg{MimeMessagePart{mime_object()}.get_message()}; !msg) return Err(Error::Code::Message, "failed to get message-part"); else return msg->to_file(path, overwrite); } else return mime_object().to_file(path, overwrite); } bool MessagePart::is_signed() const noexcept { return mime_object().is_multipart_signed(); } bool MessagePart::is_encrypted() const noexcept { return mime_object().is_multipart_encrypted(); } bool /* heuristic */ MessagePart::looks_like_attachment() const noexcept { auto matches=[](const MimeContentType& ctype, const std::initializer_list<std::pair<const char*, const char*>>& ctypes) { return std::find_if(ctypes.begin(), ctypes.end(), [&](auto&& item){ return ctype.is_type(item.first, item.second); }) != ctypes.end(); }; const auto ctype{mime_object().content_type()}; if (!ctype) return false; // no content-type: not an attachment. // we consider some parts _not_ to be attachments regardless of disposition if (matches(*ctype,{{"application", "pgp-keys"}})) return false; // we consider some parts to be attachments regardless of disposition if (matches(*ctype,{{"image", "*"}, {"audio", "*"}, {"application", "*"}, {"application", "x-patch"}})) return true; // otherwise, rely on the disposition return is_attachment(); } #ifdef BUILD_TESTS #include "utils/mu-test-utils.hh" static void test_cooked_full() { std::array<std::pair<std::string, std::string>, 4> cases = {{ { "/hello/world/foo", "foo" }, { "foo:/\n/bar", "bar"}, { "Aap Noot Mies", "Aap-Noot-Mies"}, { "..", "-"} }}; for (auto&& test: cases) assert_equal(cook_full(test.first), test.second); } static void test_cooked_minimal() { std::array<std::pair<std::string, std::string>, 4> cases = {{ { "/hello/world/foo", "foo" }, { "foo:/\n/bar", "bar"}, { "Aap Noot Mies.doc", "Aap Noot Mies.doc"}, { "..", "-"} }}; for (auto&& test: cases) assert_equal(cook_minimal(test.first), test.second); } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_test_add_func("/message/message-part/cooked-full", test_cooked_full); g_test_add_func("/message/message-part/cooked-minimal", test_cooked_minimal); return g_test_run(); } #endif /*BUILD_TESTS*/ ���������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-message-part.hh������������������������������������������������������������0000664�0000000�0000000�00000007553�14523230065�0017756�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2023 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_MESSAGE_PART_HH__ #define MU_MESSAGE_PART_HH__ #include <string> #include <memory> #include <utils/mu-option.hh> #include <utils/mu-result.hh> namespace Mu { class MimeObject; // forward declaration; don't want to include for build-time // reasons. class MessagePart { public: /** * Construct MessagePart from a MimeObject * * @param obj */ MessagePart(const MimeObject& obj); /** * Copy CTOR * * @param other */ MessagePart(const MessagePart& other); /** * DTOR * */ ~MessagePart(); /** * Get the underlying MimeObject; you need to include mu-mime-object.hh * to do anything useful with it. * * @return reference to the mime-object */ const MimeObject& mime_object() const noexcept; /** * Filename for the mime-part file. This is a "cooked" filename with * unallowed characters removed. If there's no filename specified, * construct one (such as in the case of a MimeMessagePart). * * @param minimal if true, only perform *minimal* cookiing, where we * only remove forward-slashes. * * @see raw_filename() * * @return the name */ Option<std::string> cooked_filename(bool minimal=false) const noexcept; /** * Name for the mime-part file, i.e., MimePart::filename * * @return the filename or Nothing if there is none */ Option<std::string> raw_filename() const noexcept; /** * Mime-type for the mime-part (e.g. "text/plain") * * @return the mime-part or Nothing if there is none */ Option<std::string> mime_type() const noexcept; /** * Get the content description for this part, or Nothing * * @return the content description */ Option<std::string> content_description() const noexcept; /** * Get the length of the (unencoded) MIME-part. * * @return the size */ size_t size() const noexcept; /** * Does this part have an "attachment" disposition? Otherwise it is * "inline". Note that does *not* map 1:1 to a message's HasAttachment * flag (which uses looks_like_attachment()) * * @return true or false. */ bool is_attachment() const noexcept; /** * Does this part appear to be an attachment from an end-users point of * view? This uses some heuristics to guess. Some parts for which * is_attachment() is true may not "really" be attachments, and * vice-versa * * @return true or false. */ bool looks_like_attachment() const noexcept; /** * Is this part signed? * * @return true or false */ bool is_signed() const noexcept; /** * Is this part encrypted? * * @return true or false */ bool is_encrypted() const noexcept; /** * Write (decoded) mime-part contents to string * * @return a string or nothing if there is no contemt */ Option<std::string> to_string() const noexcept; /** * Write (decoded) mime part to a file * * @param path path to file * @param overwrite whether to possibly overwrite * * @return size of file or or an error. */ Result<size_t> to_file(const std::string& path, bool overwrite) const noexcept; struct Private; private: const std::unique_ptr<MimeObject> mime_obj; }; } // namespace Mu #endif /* MU_MESSAGE_PART_HH__ */ �����������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-message.cc�����������������������������������������������������������������0000664�0000000�0000000�00000050360�14523230065�0016772�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022-2023 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-message.hh" #include "gmime/gmime-references.h" #include "gmime/gmime-stream-mem.h" #include "mu-maildir.hh" #include <array> #include <string> #include <regex> #include <utils/mu-utils.hh> #include <utils/mu-error.hh> #include <utils/mu-option.hh> #include <atomic> #include <mutex> #include <cstdlib> #include <glib.h> #include <glib/gstdio.h> #include <gmime/gmime.h> #include "gmime/gmime-message.h" #include "mu-mime-object.hh" using namespace Mu; struct Message::Private { Private(Message::Options options): opts{options} {} Private(Message::Options options, Xapian::Document&& xdoc): opts{options}, doc{std::move(xdoc)} {} Message::Options opts; Document doc; mutable Option<MimeMessage> mime_msg; Flags flags{}; Option<std::string> mailing_list; std::vector<Part> parts; ::time_t ctime{}; std::string cache_path; /* * we only need to index these, so we don't * really need these copy if we re-arrange things * a bit */ Option<std::string> body_txt; Option<std::string> body_html; Option<std::string> embedded; }; static void fill_document(Message::Private& priv); static Result<struct stat> get_statbuf(const std::string& path, Message::Options opts = Message::Options::None) { if (none_of(opts & Message::Options::AllowRelativePath) && !g_path_is_absolute(path.c_str())) return Err(Error::Code::File, "path '%s' is not absolute", path.c_str()); if (::access(path.c_str(), R_OK) != 0) return Err(Error::Code::File, "file @ '%s' is not readable", path.c_str()); struct stat statbuf{}; if (::stat(path.c_str(), &statbuf) < 0) return Err(Error::Code::File, "cannot stat %s: %s", path.c_str(), g_strerror(errno)); if (!S_ISREG(statbuf.st_mode)) return Err(Error::Code::File, "not a regular file: %s", path.c_str()); return Ok(std::move(statbuf)); } Message::Message(const std::string& path, Message::Options opts): priv_{std::make_unique<Private>(opts)} { const auto statbuf{get_statbuf(path, opts)}; if (!statbuf) throw statbuf.error(); priv_->ctime = statbuf->st_ctime; init_gmime(); if (auto msg{MimeMessage::make_from_file(path)}; !msg) throw msg.error(); else priv_->mime_msg = std::move(msg.value()); auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), NULL))}; if (xpath) priv_->doc.add(Field::Id::Path, std::move(xpath.value())); priv_->doc.add(Field::Id::Size, static_cast<int64_t>(statbuf->st_size)); // rest of the fields fill_document(*priv_); } Message::Message(const std::string& text, const std::string& path, Message::Options opts): priv_{std::make_unique<Private>(opts)} { if (text.empty()) throw Error{Error::Code::InvalidArgument, "text must not be empty"}; if (!path.empty()) { auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), {}))}; if (xpath) priv_->doc.add(Field::Id::Path, std::move(xpath.value())); } priv_->ctime = ::time({}); priv_->doc.add(Field::Id::Size, static_cast<int64_t>(text.size())); init_gmime(); if (auto msg{MimeMessage::make_from_text(text)}; !msg) throw msg.error(); else priv_->mime_msg = std::move(msg.value()); fill_document(*priv_); } Message::Message(Message&& other) noexcept { *this = std::move(other); } Message& Message::operator=(Message&& other) noexcept { if (this != &other) priv_ = std::move(other.priv_); return *this; } Message::Message(Xapian::Document&& doc): priv_{std::make_unique<Private>(Message::Options::None, std::move(doc))} {} Message::~Message() = default; const Mu::Document& Message::document() const { return priv_->doc; } unsigned Message::docid() const { return priv_->doc.xapian_document().get_docid(); } const Mu::Sexp& Message::sexp() const { return priv_->doc.sexp(); } Result<void> Message::set_maildir(const std::string& maildir) { /* sanity check a little bit */ if (maildir.empty() || maildir.at(0) != '/' || (maildir.size() > 1 && maildir.at(maildir.length()-1) == '/')) return Err(Error::Code::Message, "'%s' is not a valid maildir", maildir.c_str()); const auto path{document().string_value(Field::Id::Path)}; if (path == maildir || path.find(maildir) == std::string::npos) return Err(Error::Code::Message, "'%s' is not a valid maildir for message @ %s", maildir.c_str(), path.c_str()); priv_->doc.remove(Field::Id::Maildir); priv_->doc.add(Field::Id::Maildir, maildir); return Ok(); } void Message::set_flags(Flags flags) { priv_->doc.remove(Field::Id::Flags); priv_->doc.add(flags); } bool Message::load_mime_message(bool reload) const { if (priv_->mime_msg && !reload) return true; const auto path{document().string_value(Field::Id::Path)}; if (auto mime_msg{MimeMessage::make_from_file(path)}; !mime_msg) { g_warning("failed to load '%s': %s", path.c_str(), mime_msg.error().what()); return false; } else { priv_->mime_msg = std::move(mime_msg.value()); fill_document(*priv_); return true; } } void Message::unload_mime_message() const { priv_->mime_msg = Nothing; } bool Message::has_mime_message() const { return !!priv_->mime_msg; } static Priority get_priority(const MimeMessage& mime_msg) { constexpr std::array<std::pair<std::string_view, Priority>, 10> prio_alist = {{ {"high", Priority::High}, {"1", Priority::High}, {"2", Priority::High}, {"normal", Priority::Normal}, {"3", Priority::Normal}, {"low", Priority::Low}, {"list", Priority::Low}, {"bulk", Priority::Low}, {"4", Priority::Low}, {"5", Priority::Low} }}; const auto opt_str = mime_msg.header("Precedence") .disjunction(mime_msg.header("X-Priority")) .disjunction(mime_msg.header("Importance")); if (!opt_str) return Priority::Normal; const auto it = seq_find_if(prio_alist, [&](auto&& item) { return g_ascii_strncasecmp(item.first.data(), opt_str->c_str(), item.first.size()) == 0; }); return it == prio_alist.cend() ? Priority::Normal : it->second; } /* see: http://does-not-exist.org/mail-archives/mutt-dev/msg08249.html */ static std::vector<std::string> extract_tags(const MimeMessage& mime_msg) { constexpr std::array<std::pair<const char*, char>, 3> tag_headers = {{ {"X-Label", ' '}, {"X-Keywords", ','}, {"Keywords", ' '} }}; std::vector<std::string> tags; seq_for_each(tag_headers, [&](auto&& item) { if (auto&& hdr = mime_msg.header(item.first); hdr) { for (auto&& tagval : split(*hdr, item.second)) { tagval.erase(0, tagval.find_first_not_of(' ')); tagval.erase(tagval.find_last_not_of(' ')+1); tags.emplace_back(std::move(tagval)); } } }); return tags; } static Option<std::string> get_mailing_list(const MimeMessage& mime_msg) { char *dechdr, *res; const char *b, *e; const auto hdr{mime_msg.header("List-Id")}; if (!hdr) return {}; dechdr = g_mime_utils_header_decode_phrase(NULL, hdr->c_str()); if (!dechdr) return {}; 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 to_string_opt_gchar(std::move(res)); } static void append_text(Option<std::string>& str, Option<std::string> app) { if (!str) str = app; else if (app) str.value() += app.value(); } static void accumulate_text(const MimePart& part, Message::Private& info, const MimeContentType& ctype) { if (!ctype.is_type("text", "*")) return; /* not a text type */ if (part.is_attachment()) append_text(info.embedded, part.to_string()); else if (ctype.is_type("text", "plain")) append_text(info.body_txt, part.to_string()); else if (ctype.is_type("text", "html")) append_text(info.body_html, part.to_string()); } static bool /* heuristic */ looks_like_attachment(const MimeObject& parent, const MessagePart& mpart) { if (parent) { /* crypto multipart children are not considered attachments */ if (const auto parent_ctype{parent.content_type()}; parent_ctype) { if (parent_ctype->is_type("multipart", "signed") || parent_ctype->is_type("multipart", "encrypted")) return false; } } return mpart.looks_like_attachment(); } static void process_part(const MimeObject& parent, const MimePart& part, Message::Private& info, const MessagePart& mpart) { const auto ctype{part.content_type()}; if (!ctype) return; // flag as calendar, if not already if (none_of(info.flags & Flags::Calendar) && ctype->is_type("text", "calendar")) info.flags |= Flags::Calendar; // flag as attachment, if not already. if (none_of(info.flags & Flags::HasAttachment) && looks_like_attachment(parent, mpart)) info.flags |= Flags::HasAttachment; // if there are text parts, gather. accumulate_text(part, info, *ctype); } static void process_message_part(const MimeMessagePart& msg_part, Message::Private& info) { auto submsg{msg_part.get_message()}; if (!submsg) return; submsg->for_each([&](auto&& parent, auto&& child_obj) { /* XXX: we only handle one level */ if (!child_obj.is_part()) return; const auto ctype{child_obj.content_type()}; if (!ctype || !ctype->is_type("text", "*")) return; append_text(info.embedded, MimePart{child_obj}.to_string()); }); } static void handle_object(const MimeObject& parent, const MimeObject& obj, Message::Private& info); static void handle_encrypted(const MimeMultipartEncrypted& part, Message::Private& info) { if (!any_of(info.opts & Message::Options::Decrypt)) { /* just added to the list */ info.parts.emplace_back(part); return; } const auto proto{part.content_type_parameter("protocol").value_or("unknown")}; const auto ctx = MimeCryptoContext::make(proto); if (!ctx) { g_warning("failed to create context for protocol <%s>", proto.c_str()); return; } auto res{part.decrypt(*ctx)}; if (!res) { g_warning("failed to decrypt: %s", res.error().what()); return; } if (res->first.is_multipart()) { MimeMultipart{res->first}.for_each( [&](auto&& parent, auto&& child_obj) { handle_object(parent, child_obj, info); }); } else handle_object(part, res->first, info); } static void handle_object(const MimeObject& parent, const MimeObject& obj, Message::Private& info) { /* if it's an encrypted part we should decrypt, recurse */ if (obj.is_multipart_encrypted()) handle_encrypted(MimeMultipartEncrypted{obj}, info); else if (obj.is_part() || obj.is_message_part() || obj.is_multipart_signed() || obj.is_multipart_encrypted()) info.parts.emplace_back(obj); if (obj.is_part()) process_part(parent, obj, info, info.parts.back()); else if (obj.is_message_part()) process_message_part(obj, info); else if (obj.is_multipart_signed()) info.flags |= Flags::Signed; else if (obj.is_multipart_encrypted()) { /* FIXME: An encrypted part might be signed at the same time. * In that case the signed flag is lost. */ info.flags |= Flags::Encrypted; } else if (obj.is_mime_application_pkcs7_mime()) { MimeApplicationPkcs7Mime smime(obj); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" // CompressedData, CertsOnly, Unknown switch (smime.smime_type()) { case Mu::MimeApplicationPkcs7Mime::SecureMimeType::SignedData: info.flags |= Flags::Signed; break; case Mu::MimeApplicationPkcs7Mime::SecureMimeType::EnvelopedData: info.flags |= Flags::Encrypted; break; default: break; } #pragma GCC diagnostic pop } } /** * This message -- recursively walk through message, and initialize some * other values that depend on another. * * @param mime_msg * @param path * @param info */ static void process_message(const MimeMessage& mime_msg, const std::string& path, Message::Private& info) { /* only have file-flags when there's a path. */ if (!path.empty()) { info.flags = flags_from_path(path).value_or(Flags::None); /* pseudo-flag --> unread means either NEW or NOT SEEN, just * for searching convenience */ if (any_of(info.flags & Flags::New) || none_of(info.flags & Flags::Seen)) info.flags |= Flags::Unread; } // parts mime_msg.for_each([&](auto&& parent, auto&& child_obj) { handle_object(parent, child_obj, info); }); // get the mailing here, and use it do update flags, too. info.mailing_list = get_mailing_list(mime_msg); if (info.mailing_list) info.flags |= Flags::MailingList; } static Mu::Result<std::string> calculate_sha256(const std::string& path) { g_autoptr(GChecksum) checksum{g_checksum_new(G_CHECKSUM_SHA256)}; FILE *file{::fopen(path.c_str(), "r")}; if (!file) return Err(Error{Error::Code::File, "failed to open %s: %s", path.c_str(), ::strerror(errno)}); std::array<uint8_t, 4096> buf{}; while (true) { const auto n = ::fread(buf.data(), 1, buf.size(), file); if (n == 0) break; g_checksum_update(checksum, buf.data(), n); } bool has_err = ::ferror(file) != 0; ::fclose(file); if (has_err) return Err(Error{Error::Code::File, "failed to read %s", path.c_str()}); return Ok(g_checksum_get_string(checksum)); } /** * Get a fake-message-id for a message without one. * * @param path message path * * @return a fake message-id */ static std::string fake_message_id(const std::string& path) { constexpr auto mu_suffix{"@mu.id"}; // not a very good message-id, only for testing. if (path.empty() || ::access(path.c_str(), R_OK) != 0) return format("%08x%s", g_str_hash(path.c_str()), mu_suffix); if (const auto sha256_res{calculate_sha256(path)}; !sha256_res) return format("%08x%s", g_str_hash(path.c_str()), mu_suffix); else return format("%s%s", sha256_res.value().c_str(), mu_suffix); } /* many of the doc.add(fiels ....) automatically update the sexp-list as well; * however, there are some _extra_ values in the sexp-list that are not * based on a field. So we add them here. */ static void doc_add_list_post(Document& doc, const MimeMessage& mime_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 auto list_post{mime_msg.header("List-Post")}; if (!list_post) return; rx = g_regex_new("<?mailto:([a-z0-9!@#$%&'*+-/=?^_`{|}~]+)>?", G_REGEX_CASELESS, (GRegexMatchFlags)0, {}); g_return_if_fail(rx); Contacts contacts; if (g_regex_match(rx, list_post->c_str(), (GRegexMatchFlags)0, &minfo)) { auto address = (char*)g_match_info_fetch(minfo, 1); contacts.push_back(Contact(address)); g_free(address); } g_match_info_free(minfo); g_regex_unref(rx); doc.add_extra_contacts(":list-post", contacts); } static void doc_add_reply_to(Document& doc, const MimeMessage& mime_msg) { doc.add_extra_contacts(":reply-to", mime_msg.contacts(Contact::Type::ReplyTo)); } static void fill_document(Message::Private& priv) { /* hunt & gather info from message tree */ Document& doc{priv.doc}; MimeMessage& mime_msg{priv.mime_msg.value()}; const auto path{doc.string_value(Field::Id::Path)}; const auto refs{mime_msg.references()}; const auto& raw_message_id = mime_msg.message_id(); const auto message_id = raw_message_id.has_value() && !raw_message_id->empty() ? *raw_message_id : fake_message_id(path); process_message(mime_msg, path, priv); doc_add_list_post(doc, mime_msg); /* only in sexp */ doc_add_reply_to(doc, mime_msg); /* only in sexp */ field_for_each([&](auto&& field) { /* insist on expliclity handling each */ #pragma GCC diagnostic push #pragma GCC diagnostic error "-Wswitch" switch(field.id) { case Field::Id::Bcc: doc.add(field.id, mime_msg.contacts(Contact::Type::Bcc)); break; case Field::Id::BodyText: doc.add(field.id, priv.body_txt); break; case Field::Id::Cc: doc.add(field.id, mime_msg.contacts(Contact::Type::Cc)); break; case Field::Id::Changed: doc.add(field.id, priv.ctime); break; case Field::Id::Date: doc.add(field.id, mime_msg.date()); break; case Field::Id::EmbeddedText: doc.add(field.id, priv.embedded); break; case Field::Id::File: for (auto&& part: priv.parts) doc.add(field.id, part.raw_filename()); break; case Field::Id::Flags: doc.add(priv.flags); break; case Field::Id::From: doc.add(field.id, mime_msg.contacts(Contact::Type::From)); break; case Field::Id::Maildir: /* already */ break; case Field::Id::MailingList: doc.add(field.id, priv.mailing_list); break; case Field::Id::MessageId: doc.add(field.id, message_id); break; case Field::Id::MimeType: for (auto&& part: priv.parts) doc.add(field.id, part.mime_type()); break; case Field::Id::Path: /* already */ break; case Field::Id::Priority: doc.add(get_priority(mime_msg)); break; case Field::Id::References: if (!refs.empty()) doc.add(field.id, refs); break; case Field::Id::Size: /* already */ break; case Field::Id::Subject: doc.add(field.id, mime_msg.subject().map(remove_ctrl)); break; case Field::Id::Tags: if (auto&& tags{extract_tags(mime_msg)}; !tags.empty()) doc.add(field.id, tags); break; case Field::Id::ThreadId: // either the oldest reference, or otherwise the message id doc.add(field.id, refs.empty() ? message_id : refs.at(0)); break; case Field::Id::To: doc.add(field.id, mime_msg.contacts(Contact::Type::To)); break; /* internal fields */ case Field::Id::XBodyHtml: doc.add(field.id, priv.body_html); break; /* LCOV_EXCL_START */ case Field::Id::_count_: default: break; /* LCOV_EXCL_STOP */ } #pragma GCC diagnostic pop }); } Option<std::string> Message::header(const std::string& header_field) const { load_mime_message(); return priv_->mime_msg->header(header_field); } Option<std::string> Message::body_text() const { load_mime_message(); return priv_->body_txt; } Option<std::string> Message::body_html() const { load_mime_message(); return priv_->body_html; } Contacts Message::all_contacts() const { Contacts contacts; if (!load_mime_message()) return contacts; /* empty */ return priv_->mime_msg->contacts(Contact::Type::None); /* get all types */ } const std::vector<Message::Part>& Message::parts() const { if (!load_mime_message()) { static std::vector<Message::Part> empty; return empty; } return priv_->parts; } Result<std::string> Message::cache_path(Option<size_t> index) const { /* create tmpdir for this message, if needed */ if (priv_->cache_path.empty()) { GError *err{}; auto tpath{to_string_opt_gchar(g_dir_make_tmp("mu-cache-XXXXXX", &err))}; if (!tpath) return Err(Error::Code::File, &err, "failed to create temp dir"); priv_->cache_path = std::move(tpath.value()); } if (index) { GError *err{}; auto tpath = format("%s/%zu", priv_->cache_path.c_str(), *index); if (g_mkdir(tpath.c_str(), 0700) != 0) return Err(Error::Code::File, &err, "failed to create cache dir '%s'; err=%d", tpath.c_str(), errno); return Ok(std::move(tpath)); } else return Ok(std::string{priv_->cache_path}); } // for now this only remove stray '/' at the end std::string Message::sanitize_maildir(const std::string& mdir) { if (mdir.size() > 1 && mdir.at(mdir.length()-1) == '/') return mdir.substr(0, mdir.length() - 1); else return mdir; } Result<void> Message::update_after_move(const std::string& new_path, const std::string& new_maildir, Flags new_flags) { if (auto statbuf{get_statbuf(new_path)}; !statbuf) return Err(statbuf.error()); else priv_->ctime = statbuf->st_ctime; priv_->doc.remove(Field::Id::Path); priv_->doc.remove(Field::Id::Changed); priv_->doc.add(Field::Id::Path, new_path); priv_->doc.add(Field::Id::Changed, priv_->ctime); set_flags(new_flags); if (const auto res = set_maildir(sanitize_maildir(new_maildir)); !res) return res; return Ok(); } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-message.hh�����������������������������������������������������������������0000664�0000000�0000000�00000027261�14523230065�0017010�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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_MESSAGE_HH__ #define MU_MESSAGE_HH__ #include <memory> #include <string> #include <vector> #include <iostream> #include "mu-contact.hh" #include "mu-priority.hh" #include "mu-flags.hh" #include "mu-fields.hh" #include "mu-document.hh" #include "mu-message-part.hh" #include "mu-message-file.hh" #include <xapian.h> #include "utils/mu-utils.hh" #include "utils/mu-option.hh" #include "utils/mu-result.hh" #include "utils/mu-sexp.hh" namespace Mu { class Message { public: enum struct Options { None = 0, /**< Defaults */ Decrypt = 1 << 0, /**< Attempt to decrypt */ RetrieveKeys = 1 << 1, /**< Auto-retrieve crypto keys (implies network * access) */ AllowRelativePath = 1 << 2, /**< Allow relateive paths for filename * in make_from_path */ }; /** * Move CTOR * * @param some other message */ Message(Message&& other) noexcept; /** * operator= * * @param other move some object object * * @return */ Message& operator=(Message&& other) noexcept; /** * Construct a message based on a path * * @param path path to message * @param opts options * * @return a message or an error */ static Result<Message> make_from_path(const std::string& path, Options opts={}) try { return Ok(Message{path,opts}); } catch (Error& err) { return Err(err); } /* LCOV_EXCL_START */ catch (...) { return Err(Mu::Error(Error::Code::Message, "failed to create message from path")); } /* LCOV_EXCL_STOP */ /** * Construct a message based on a Xapian::Document * * @param doc a Mu Document * * @return a message or an error */ static Result<Message> make_from_document(Xapian::Document&& doc) try { return Ok(Message{std::move(doc)}); } catch (Error& err) { return Err(err); } /* LCOV_EXCL_START */ catch (...) { return Err(Mu::Error(Error::Code::Message, "failed to create message from document")); } /* LCOV_EXCL_STOP */ /** * Construct a message from a string. This is mostly useful for testing. * * @param text message text * @param path path to message - optional; path does not have to exist. * @param opts options * * @return a message or an error */ static Result<Message> make_from_text(const std::string& text, const std::string& path={}, Options opts={}) try { return Ok(Message{text, path, opts}); } catch (Error& err) { return Err(err); } /* LCOV_EXCL_START */ catch (...) { return Err(Mu::Error(Error::Code::Message, "failed to create message from text")); } /* LCOV_EXCL_STOP */ /** * DTOR */ ~Message(); /** * Get the document. * * * @return document */ const Document& document() const; /** * Get the document-id, or 0 if non-existent. * * @return document id */ unsigned docid() const; /** * Get the file system path of this message * * @return the path of this Message or NULL in case of error. * the returned string should *not* be modified or freed. */ std::string path() const { return document().string_value(Field::Id::Path); } /** * Get the sender (From:) of this message * * @return the sender(s) of this Message */ Contacts from() const { return document().contacts_value(Field::Id::From); } /** * Get the recipient(s) (To:) for this message * * @return recipients */ Contacts to() const { return document().contacts_value(Field::Id::To); } /** * Get the recipient(s) (Cc:) for this message * * @return recipients */ Contacts cc() const { return document().contacts_value(Field::Id::Cc); } /** * Get the recipient(s) (Bcc:) for this message * * @return recipients */ Contacts bcc() const { return document().contacts_value(Field::Id::Bcc); } /** * Get the maildir this message resides in; i.e., if the path is * ~/Maildir/foo/bar/cur/msg, the maildir would typically be foo/bar * * This is determined when _storing_ the message (which uses * set_maildir()) * * @return the maildir requested or empty */ std::string maildir() const { return document().string_value(Field::Id::Maildir); } /** * Set the maildir for this message. This is for use by the _store_ when * it has determined the maildir for this message from the message's path and * the root-maildir known by the store. * * @param maildir the maildir for this message * * @return Ok() or some error if the maildir is invalid */ Result<void> set_maildir(const std::string& maildir); /** * Clean up the maildir. This is for internal use, but exposed for testing. * For now cleaned-up means "stray trailing / removed". * * @param maildir some maildir * * @return a cleaned-up version */ static std::string sanitize_maildir(const std::string& maildir); /** * Get the subject of this message * * @return the subject of this Message */ std::string subject() const { return document().string_value(Field::Id::Subject); } /** * Get the Message-Id of this message * * @return the Message-Id of this message (without the enclosing <>), or * a fake message-id for messages that don't have them. * * For file-backed message, this fake message-id is based on a hash of the * message contents. For non-file-backed (test) messages, some other value * is concocted. */ std::string message_id() const { return document().string_value(Field::Id::MessageId);} /** * get the mailing list for a message, i.e. the mailing-list * identifier in the List-Id header. * * @return the mailing list id for this message (without the enclosing <>) * or NULL in case of error or if there is none. */ std::string mailing_list() const { return document().string_value(Field::Id::MailingList);} /** * get the message date/time (the Date: field) as time_t * * @return message date/time or 0 in case of error or if there * is no such header. */ ::time_t date() const { return static_cast<::time_t>(document().integer_value(Field::Id::Date)); } /** * get the last change-time this message. For path/document-based * messages this corresponds with the ctime of the underlying file; for * the text-based ones (as used for testing) it is the creation time. * * @return last-change time or 0 if unknown */ ::time_t changed() const { return static_cast<::time_t>(document().integer_value(Field::Id::Changed)); } /** * get the flags for this message. * * @return the file/content flags */ Flags flags() const { return document().flags_value(); } /** * Update the flags for this message. This is useful for flags * that can only be determined after the message has been created already, * such as the 'personal' flag. * * @param flags new flags. */ void set_flags(Flags flags); /** * get the message priority for this message. The X-Priority, X-MSMailPriority, * Importance and Precedence header are checked, in that order. if no known or * explicit priority is set, Priority::Id::Normal is assumed * * @return the message priority */ Priority priority() const { return document().priority_value(); } /** * get the file size in bytes of this message * * @return the filesize */ size_t size() const { return static_cast<size_t>(document().integer_value(Field::Id::Size)); } /** * Get the (possibly empty) 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 and fake-message-id (see impls) are * filtered out. * * @return a vec with the references for this msg. */ std::vector<std::string> references() const { return document().string_vec_value(Field::Id::References); } /** * Get the thread-id for this message. This is the message-id of the * oldest-known (grand) parent, or the message-id of this message if * none. * * @return the thread id. */ std::string thread_id() const { return document().string_value(Field::Id::ThreadId); } /** * 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 */ std::vector<std::string> tags() const { return document() .string_vec_value(Field::Id::Tags); } /* * Convert to Sexp */ /** * Get the s-expression for this message. Stays valid as long as this * message is. * * @return an Sexp representing the message. */ const Sexp& sexp() const; /* * And some non-const message, for updating an existing * message after a file-system move. * * @return Ok or an error. */ Result<void> update_after_move(const std::string& new_path, const std::string& new_maildir, Flags new_flags); /* * Below require a file-backed message, which is a relatively slow * if there isn't one already; see load_mime_message() */ /** * Get the text body * * @return text body */ Option<std::string> body_text() const; /** * Get the HTML body * * @return text body */ Option<std::string> body_html() const; /** * Get some message-header * * @param header_field name of the header * * @return the value (UTF-8), or Nothing. */ Option<std::string> header(const std::string& header_field) const; /** * Get all contacts for this message. * * @return contacts */ Contacts all_contacts() const; /** * Get information about MIME-parts in this message. * * @return mime-part info. */ using Part = MessagePart; const std::vector<Part>& parts() const; /** * Get the path to a cche directory for this message, which * is useful for temporarily saving attachments * * @param index optionally, create <cache-path>/<index> instead; * this is useful for having part-specific subdirectories. * * @return path to a (created) cache directory, or an error. */ Result<std::string> cache_path(Option<size_t> index={}) const; /** * Load the GMime (file) message (for a database-backed message), * if not already (but see @param reload). * * Affects cached-state only, so we still mark this as 'const' * * @param reload whether to force reloading (even if already) * * @return true if loading worked; false otherwise. */ bool load_mime_message(bool reload=false) const; /** * Clear the GMime message. * * Affects cached-state only, so we still mark this as 'const' */ void unload_mime_message() const; /** * Has a (file-base) GMime message been loaded? * * * @return true or false */ bool has_mime_message() const; struct Private; /* * Usually the make_ builders are better to create a message, but in * some special cases, we need a heap-allocated message... */ Message(Xapian::Document&& xdoc); Message(const std::string& path, Options opts); private: Message(const std::string& str, const std::string& path, Options opt); std::unique_ptr<Private> priv_; }; // Message MU_ENABLE_BITOPS(Message::Options); static inline std::ostream& operator<<(std::ostream& os, const Message& msg) { os << msg.sexp(); return os; } } // Mu #endif /* MU_MESSAGE_HH__ */ �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-mime-object.cc�������������������������������������������������������������0000664�0000000�0000000�00000051417�14523230065�0017545�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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-mime-object.hh" #include "gmime/gmime-message.h" #include "utils/mu-utils.hh" #include <mutex> #include <regex> #include <fcntl.h> #include <sys/stat.h> #include <errno.h> using namespace Mu; /* 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. */ void Mu::init_gmime(void) { // fast path. static bool gmime_initialized = false; if (gmime_initialized) return; static std::mutex gmime_lock; std::lock_guard lock (gmime_lock); if (gmime_initialized) return; // already g_debug("initializing gmime %u.%u.%u", gmime_major_version, gmime_minor_version, gmime_micro_version); g_mime_init(); gmime_initialized = true; std::atexit([] { g_debug("shutting down gmime"); g_mime_shutdown(); gmime_initialized = false; }); } std::string Mu::address_rfc2047(const Contact& contact) { init_gmime(); InternetAddress *addr = internet_address_mailbox_new(contact.name.c_str(), contact.email.c_str()); std::string encoded = to_string_gchar( internet_address_to_string(addr, {}, true)); g_object_unref(addr); return encoded; } /* * MimeObject */ Option<std::string> MimeObject::header(const std::string& hdr) const noexcept { if (auto val{g_mime_object_get_header(self(), hdr.c_str())}; !val) return Nothing; else if (!g_utf8_validate(val, -1, {})) return utf8_clean(val); else return std::string{val}; } std::vector<std::pair<std::string, std::string>> MimeObject::headers() const noexcept { GMimeHeaderList *lst; lst = g_mime_object_get_header_list(self()); /* _not_ owned */ if (!lst) return {}; std::vector<std::pair<std::string, std::string>> hdrs; const auto hdr_num{g_mime_header_list_get_count(lst)}; for (int i = 0; i != hdr_num; ++i) { GMimeHeader *hdr{g_mime_header_list_get_header_at(lst, i)}; if (!hdr) /* ^^^ _not_ owned */ continue; const auto name{g_mime_header_get_name(hdr)}; const auto val{g_mime_header_get_value(hdr)}; if (!name || !val) continue; hdrs.emplace_back(name, val); } return hdrs; } Result<size_t> MimeObject::write_to_stream(const MimeFormatOptions& f_opts, MimeStream& stream) const { auto written = g_mime_object_write_to_stream(self(), f_opts.get(), GMIME_STREAM(stream.object())); if (written < 0) return Err(Error::Code::File, "failed to write mime-object to stream"); else return Ok(static_cast<size_t>(written)); } Result<size_t> MimeObject::to_file(const std::string& path, bool overwrite) const noexcept { GError *err{}; auto strm{g_mime_stream_fs_open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC |(overwrite ? 0 : O_EXCL), S_IRUSR|S_IWUSR, &err)}; if (!strm) return Err(Error::Code::File, &err, "failed to open '%s'", path.c_str()); MimeStream stream{MimeStream::make_from_stream(strm)}; return write_to_stream({}, stream); } Option<std::string> MimeObject::to_string_opt() const noexcept { auto stream{MimeStream::make_mem()}; if (!stream) { g_warning("failed to create mem stream"); return Nothing; } const auto written = g_mime_object_write_to_stream( self(), {}, GMIME_STREAM(stream.object())); if (written < 0) { g_warning("failed to write object to stream"); return Nothing; } std::string buffer; buffer.resize(written + 1); stream.reset(); auto bytes{g_mime_stream_read(GMIME_STREAM(stream.object()), buffer.data(), written)}; if (bytes < 0) return Nothing; buffer.data()[written]='\0'; buffer.resize(written); return buffer; } /* * MimeCryptoContext */ Result<size_t> MimeCryptoContext::import_keys(MimeStream& stream) { GError *err{}; auto res = g_mime_crypto_context_import_keys( self(), GMIME_STREAM(stream.object()), &err); if (res < 0) return Err(Error::Code::File, &err, "error importing keys"); return Ok(static_cast<size_t>(res)); } void MimeCryptoContext::set_request_password(PasswordRequestFunc pw_func) { static auto request_func = pw_func; g_mime_crypto_context_set_request_password( self(), [](GMimeCryptoContext *ctx, const char *user_id, const char *prompt, gboolean reprompt, GMimeStream *response, GError **err) -> gboolean { MimeStream mstream{MimeStream::make_from_stream(response)}; auto res = request_func(MimeCryptoContext(ctx), std::string{user_id ? user_id : ""}, std::string{prompt ? prompt : ""}, !!reprompt, mstream); if (res) return TRUE; res.error().fill_g_error(err); return FALSE; }); } Result<void> MimeCryptoContext::setup_gpg_test(const std::string& testpath) { /* setup clean environment for testing; inspired by gmime */ g_setenv ("GNUPGHOME", format("%s/.gnupg", testpath.c_str()).c_str(), 1); /* disable environment variables that gpg-agent uses for pinentry */ g_unsetenv ("DBUS_SESSION_BUS_ADDRESS"); g_unsetenv ("DISPLAY"); g_unsetenv ("GPG_TTY"); if (g_mkdir_with_parents((testpath + "/.gnupg").c_str(), 0700) != 0) return Err(Error::Code::File, "failed to create gnupg dir; err=%d", errno); auto write_gpgfile=[&](const std::string& fname, const std::string& data) -> Result<void> { GError *err{}; std::string path{format("%s/%s", testpath.c_str(), fname.c_str())}; if (!g_file_set_contents(path.c_str(), data.c_str(), data.size(), &err)) return Err(Error::Code::File, &err, "failed to write %s", path.c_str()); else return Ok(); }; // some more elegant way? if (auto&& res = write_gpgfile("gpg.conf", "pinentry-mode loopback\n"); !res) return res; if (auto&& res = write_gpgfile("gpgsm.conf", "disable-crl-checks\n")) return res; return Ok(); } /* * MimeMessage */ static Result<MimeMessage> make_from_stream(GMimeStream* &&stream/*consume*/) { init_gmime(); GMimeParser *parser{g_mime_parser_new_with_stream(stream)}; g_object_unref(stream); if (!parser) return Err(Error::Code::Message, "cannot create mime parser"); GMimeMessage *gmime_msg{g_mime_parser_construct_message(parser, NULL)}; g_object_unref(parser); if (!gmime_msg) return Err(Error::Code::Message, "message seems invalid"); auto mime_msg{MimeMessage{std::move(G_OBJECT(gmime_msg))}}; g_object_unref(gmime_msg); return Ok(std::move(mime_msg)); } Result<MimeMessage> MimeMessage::make_from_file(const std::string& path) { GError* err{}; init_gmime(); if (auto&& stream{g_mime_stream_file_open(path.c_str(), "r", &err)}; !stream) return Err(Error::Code::Message, &err, "failed to open stream for %s", path.c_str()); else return make_from_stream(std::move(stream)); } Result<MimeMessage> MimeMessage::make_from_text(const std::string& text) { init_gmime(); if (auto&& stream{g_mime_stream_mem_new_with_buffer( text.c_str(), text.length())}; !stream) return Err(Error::Code::Message, "failed to open stream for string"); else return make_from_stream(std::move(stream)); } Option<int64_t> MimeMessage::date() const noexcept { GDateTime *dt{g_mime_message_get_date(self())}; if (!dt) return Nothing; else return g_date_time_to_unix(dt); } constexpr Option<GMimeAddressType> address_type(Contact::Type ctype) { switch(ctype) { case Contact::Type::Bcc: return GMIME_ADDRESS_TYPE_BCC; case Contact::Type::Cc: return GMIME_ADDRESS_TYPE_CC; case Contact::Type::From: return GMIME_ADDRESS_TYPE_FROM; case Contact::Type::To: return GMIME_ADDRESS_TYPE_TO; case Contact::Type::ReplyTo: return GMIME_ADDRESS_TYPE_REPLY_TO; case Contact::Type::Sender: return GMIME_ADDRESS_TYPE_SENDER; case Contact::Type::None: default: return Nothing; } } static Mu::Contacts all_contacts(const MimeMessage& msg) { Contacts contacts; for (auto&& cctype: { Contact::Type::Sender, Contact::Type::From, Contact::Type::ReplyTo, Contact::Type::To, Contact::Type::Cc, Contact::Type::Bcc }) { auto addrs{msg.contacts(cctype)}; std::move(addrs.begin(), addrs.end(), std::back_inserter(contacts)); } return contacts; } Mu::Contacts MimeMessage::contacts(Contact::Type ctype) const noexcept { /* special case: get all */ if (ctype == Contact::Type::None) return all_contacts(*this); const auto atype{address_type(ctype)}; if (!atype) return {}; auto addrs{g_mime_message_get_addresses(self(), *atype)}; if (!addrs) return {}; const auto msgtime{date().value_or(0)}; Contacts contacts; auto lst_len{internet_address_list_length(addrs)}; contacts.reserve(lst_len); for (auto i = 0; i != lst_len; ++i) { auto&& addr{internet_address_list_get_address(addrs, i)}; const auto name{internet_address_get_name(addr)}; if (G_UNLIKELY(!INTERNET_ADDRESS_IS_MAILBOX(addr))) continue; const auto email{internet_address_mailbox_get_addr ( INTERNET_ADDRESS_MAILBOX(addr))}; if (G_UNLIKELY(!email)) continue; contacts.emplace_back(email, name ? name : "", ctype, msgtime); } return contacts; } /* * references() returns the concatenation of the References and In-Reply-To * message-ids (in that order). Duplicates are removed. * * The _first_ one in the list determines the thread-id for the message. */ std::vector<std::string> MimeMessage::references() const noexcept { // is ref already in the list? O(n) but with small n. auto is_dup = [](auto&& seq, const std::string& ref) { return seq_some(seq, [&](auto&& str) { return ref == str; }); }; auto is_fake = [](auto&& msgid) { // this is bit ugly; protonmail injects fake References which // can otherwise screw up threading. if (g_str_has_suffix(msgid, "protonmail.internalid")) return true; /* ... */ return false; }; std::vector<std::string> refs; for (auto&& ref_header: { "References", "In-reply-to" }) { auto hdr{header(ref_header)}; if (!hdr) continue; GMimeReferences *mime_refs{g_mime_references_parse({}, hdr->c_str())}; refs.reserve(refs.size() + g_mime_references_length(mime_refs)); for (auto i = 0; i != g_mime_references_length(mime_refs); ++i) { const auto msgid{g_mime_references_get_message_id(mime_refs, i)}; if (msgid && !is_dup(refs, msgid) && !is_fake(msgid)) refs.emplace_back(msgid); } g_mime_references_free(mime_refs); } return refs; } void MimeMessage::for_each(const ForEachFunc& func) const noexcept { struct CallbackData { const ForEachFunc& func; }; CallbackData cbd{func}; g_mime_message_foreach( self(), [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) { auto cb_data{reinterpret_cast<CallbackData*>(user_data)}; cb_data->func(MimeObject{parent}, MimeObject{part}); }, &cbd); } /* * MimePart */ size_t MimePart::size() const noexcept { auto wrapper{g_mime_part_get_content(self())}; if (!wrapper) { g_warning("failed to get content wrapper"); return 0; } auto stream{g_mime_data_wrapper_get_stream(wrapper)}; if (!stream) { g_warning("failed to get stream"); return 0; } return static_cast<size_t>(g_mime_stream_length(stream)); } Option<std::string> MimePart::to_string() const noexcept { /* * easy case: text. this automatically handles conversion to utf-8. */ if (GMIME_IS_TEXT_PART(self())) { if (char* txt{g_mime_text_part_get_text(GMIME_TEXT_PART(self()))}; !txt) return Nothing; else return to_string_gchar(std::move(txt)/*consumes*/); } /* * harder case: read from stream manually */ GMimeDataWrapper *wrapper{g_mime_part_get_content(self())}; if (!wrapper) { /* this happens with invalid mails */ g_debug("failed to create data wrapper"); return Nothing; } GMimeStream *stream{g_mime_stream_mem_new()}; if (!stream) { g_warning("failed to create mem stream"); return Nothing; } ssize_t buflen{g_mime_data_wrapper_write_to_stream(wrapper, stream)}; if (buflen <= 0) { /* empty buffer, not an error */ g_object_unref(stream); return Nothing; } std::string buffer; buffer.resize(buflen + 1); g_mime_stream_reset(stream); auto bytes{g_mime_stream_read(stream, buffer.data(), buflen)}; g_object_unref(stream); if (bytes < 0) return Nothing; buffer.data()[bytes]='\0'; buffer.resize(buflen); return buffer; } Result<size_t> MimePart::to_file(const std::string& path, bool overwrite) const noexcept { MimeDataWrapper wrapper{g_mime_part_get_content(self())}; if (!wrapper) /* this happens with invalid mails */ return Err(Error::Code::File, "failed to create data wrapper"); GError *err{}; auto strm{g_mime_stream_fs_open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC |(overwrite ? 0 : O_EXCL), S_IRUSR|S_IWUSR, &err)}; if (!strm) return Err(Error::Code::File, &err, "failed to open '%s'", path.c_str()); MimeStream stream{MimeStream::make_from_stream(strm)}; ssize_t written{g_mime_data_wrapper_write_to_stream( GMIME_DATA_WRAPPER(wrapper.object()), GMIME_STREAM(stream.object()))}; if (written < 0) { return Err(Error::Code::File, &err, "failed to write to '%s'", path.c_str()); } return Ok(static_cast<size_t>(written)); } void MimeMultipart::for_each(const ForEachFunc& func) const noexcept { struct CallbackData { const ForEachFunc& func; }; CallbackData cbd{func}; g_mime_multipart_foreach( self(), [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) { auto cb_data{reinterpret_cast<CallbackData*>(user_data)}; cb_data->func(MimeObject{parent}, MimeObject{part}); }, &cbd); } /* * we need to be able to pass a crypto-context to the verify(), but * g_mime_multipart_signed_verify() doesn't offer that anymore in GMime 3.x. * * So, add that by reimplementing it a bit (follow the upstream impl) */ static bool mime_types_equal (const std::string& mime_type, const std::string& official_type) { if (g_ascii_strcasecmp(mime_type.c_str(), official_type.c_str()) == 0) return true; const auto slash_pos = official_type.find("/"); if (slash_pos == std::string::npos || slash_pos == 0) return false; /* If the official mime-type's subtype already begins with "x-", then there's * nothing else to check. */ const auto subtype{official_type.substr(slash_pos + 1)}; if (g_ascii_strncasecmp (subtype.c_str(), "x-", 2) == 0) return false; const auto supertype{official_type.substr(0, slash_pos - 1)}; const auto xtype{official_type.substr(0, slash_pos - 1) + "x-" + subtype}; /* Check if the "x-" version of the official mime-type matches the * supplied mime-type. For example, if the official mime-type is * "application/pkcs7-signature", then we also want to match * "application/x-pkcs7-signature". */ return g_ascii_strcasecmp(mime_type.c_str(), xtype.c_str()) == 0; } /** * A bit of a monster, this impl. * * It's the transliteration of the g_mime_multipart_signed_verify() which * adds the feature of passing in the CryptoContext. * */ Result<std::vector<MimeSignature>> MimeMultipartSigned::verify(const MimeCryptoContext& ctx, VerifyFlags vflags) const noexcept { if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2) return Err(Error::Code::Crypto, "cannot verify, not enough subparts"); const auto proto{content_type_parameter("protocol")}; const auto sign_proto{ctx.signature_protocol()}; if (!proto || !sign_proto || !mime_types_equal(*proto, *sign_proto)) return Err(Error::Code::Crypto, "unsupported protocol " + proto.value_or("<unknown>")); const auto sig{signed_signature_part()}; const auto content{signed_content_part()}; if (!sig || !content) return Err(Error::Code::Crypto, "cannot find part"); const auto sig_mime_type{sig->mime_type()}; if (!sig || !mime_types_equal(sig_mime_type.value_or("<none>"), *sign_proto)) return Err(Error::Code::Crypto, "failed to find matching signature part"); MimeFormatOptions fopts{g_mime_format_options_new()}; g_mime_format_options_set_newline_format(fopts.get(), GMIME_NEWLINE_FORMAT_DOS); MimeStream stream{MimeStream::make_mem()}; if (auto&& res = content->write_to_stream(fopts, stream); !res) return Err(res.error()); stream.reset(); MimeDataWrapper wrapper{g_mime_part_get_content(GMIME_PART(sig->object()))}; MimeStream sigstream{MimeStream::make_mem()}; if (auto&& res = wrapper.write_to_stream(sigstream); !res) return Err(res.error()); sigstream.reset(); GError *err{}; GMimeSignatureList *siglist{g_mime_crypto_context_verify( GMIME_CRYPTO_CONTEXT(ctx.object()), static_cast<GMimeVerifyFlags>(vflags), GMIME_STREAM(stream.object()), GMIME_STREAM(sigstream.object()), {}, &err)}; if (!siglist) return Err(Error::Code::Crypto, &err, "failed to verify"); std::vector<MimeSignature> sigs; for (auto i = 0; i != g_mime_signature_list_length(siglist); ++i) { GMimeSignature *msig = g_mime_signature_list_get_signature(siglist, i); sigs.emplace_back(MimeSignature(msig)); } g_object_unref(siglist); return sigs; } std::vector<MimeCertificate> MimeDecryptResult::recipients() const noexcept { GMimeCertificateList *lst{g_mime_decrypt_result_get_recipients(self())}; if (!lst) return {}; std::vector<MimeCertificate> certs; for (int i = 0; i != g_mime_certificate_list_length(lst); ++i) certs.emplace_back( MimeCertificate( g_mime_certificate_list_get_certificate(lst, i))); return certs; } std::vector<MimeSignature> MimeDecryptResult::signatures() const noexcept { GMimeSignatureList *lst{g_mime_decrypt_result_get_signatures(self())}; if (!lst) return {}; std::vector<MimeSignature> sigs; for (auto i = 0; i != g_mime_signature_list_length(lst); ++i) { GMimeSignature *sig = g_mime_signature_list_get_signature(lst, i); sigs.emplace_back(MimeSignature(sig)); } return sigs; } /** * Like verify, a bit of a monster, this impl. * * It's the transliteration of the g_mime_multipart_encrypted_decrypt() which * adds the feature of passing in the CryptoContext. * */ Mu::Result<MimeMultipartEncrypted::Decrypted> MimeMultipartEncrypted::decrypt(const MimeCryptoContext& ctx, DecryptFlags dflags, const std::string& session_key) const noexcept { if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2) return Err(Error::Code::Crypto, "cannot decrypted, not enough subparts"); const auto proto{content_type_parameter("protocol")}; const auto enc_proto{ctx.encryption_protocol()}; if (!proto || !enc_proto || !mime_types_equal(*proto, *enc_proto)) return Err(Error::Code::Crypto, "unsupported protocol " + proto.value_or("<unknown>")); const auto version{encrypted_version_part()}; const auto encrypted{encrypted_content_part()}; if (!version || !encrypted) return Err(Error::Code::Crypto, "cannot find part"); if (!mime_types_equal(version->mime_type().value_or(""), proto.value())) return Err(Error::Code::Crypto, "cannot decrypt; unexpected version content-type '%s' != '%s'", version->mime_type().value_or("").c_str(), proto.value().c_str()); if (!mime_types_equal(encrypted->mime_type().value_or(""), "application/octet-stream")) return Err(Error::Code::Crypto, "cannot decrypt; unexpected encrypted content-type '%s'", encrypted->mime_type().value_or("").c_str()); const auto content{encrypted->content()}; auto ciphertext{MimeStream::make_mem()}; content.write_to_stream(ciphertext); ciphertext.reset(); auto stream{MimeStream::make_mem()}; auto filtered{MimeStream::make_filtered(stream)}; auto filter{g_mime_filter_dos2unix_new(FALSE)}; g_mime_stream_filter_add(GMIME_STREAM_FILTER(filtered.object()), filter); g_object_unref(filter); GError *err{}; GMimeDecryptResult *dres = g_mime_crypto_context_decrypt(GMIME_CRYPTO_CONTEXT(ctx.object()), static_cast<GMimeDecryptFlags>(dflags), session_key.empty() ? NULL : session_key.c_str(), GMIME_STREAM(ciphertext.object()), GMIME_STREAM(filtered.object()), &err); if (!dres) return Err(Error::Code::Crypto, &err, "decryption failed"); filtered.flush(); stream.reset(); auto parser{g_mime_parser_new()}; g_mime_parser_init_with_stream(parser, GMIME_STREAM(stream.object())); auto decrypted{g_mime_parser_construct_part(parser, NULL)}; g_object_unref(parser); if (!decrypted) { g_object_unref(dres); return Err(Error::Code::Crypto, "failed to parse decrypted part"); } Decrypted result = { MimeObject{decrypted}, MimeDecryptResult{dres} }; g_object_unref(decrypted); g_object_unref(dres); return Ok(std::move(result)); } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-mime-object.hh�������������������������������������������������������������0000664�0000000�0000000�00000105415�14523230065�0017555�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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_MIME_OBJECT_HH__ #define MU_MIME_OBJECT_HH__ #include <stdexcept> #include <string> #include <functional> #include <array> #include <vector> #include <gmime/gmime.h> #include "gmime/gmime-application-pkcs7-mime.h" #include "gmime/gmime-crypto-context.h" #include "utils/mu-option.hh" #include "utils/mu-result.hh" #include "utils/mu-utils.hh" #include "mu-contact.hh" namespace Mu { /* non-GObject types */ using MimeFormatOptions = deletable_unique_ptr<GMimeFormatOptions, g_mime_format_options_free>; /** * Initialize gmime (idempotent) * */ void init_gmime(void); /** * Get a RFC2047-compatible address for the given contact * * @param contact a contact * * @return an address string */ std::string address_rfc2047(const Contact& contact); class Object { public: /** * Default CTOR * */ Object() noexcept: self_{} {} /** * Create an object from a GObject * * @param obj a gobject. A ref is added. */ Object(GObject* &&obj): self_{G_OBJECT(g_object_ref(obj))} { if (!G_IS_OBJECT(obj)) throw std::runtime_error("not a g-object"); } /** * Copy CTOR * * @param other some other Object */ Object(const Object& other) noexcept { *this = other; } /** * Move CTOR * * @param other some other Object */ Object(Object&& other) noexcept { *this = std::move(other); } /** * operator= * * @param other copy some other object * * @return *this */ Object& operator=(const Object& other) noexcept { if (this != &other) { auto oldself = self_; self_ = other.self_ ? G_OBJECT(g_object_ref(other.self_)) : nullptr; if (oldself) g_object_unref(oldself); } return *this; } /** * operator= * * @param other move some object object * * @return */ Object& operator=(Object&& other) noexcept { if (this != &other) { auto oldself = self_; self_ = other.self_; other.self_ = nullptr; if (oldself) g_object_unref(oldself); } return *this; } /** * DTOR */ virtual ~Object() { if (self_) { g_object_unref(self_); } } /** * operator bool * * @return true if object wraps a GObject, false otherwise */ operator bool() const noexcept { return !!self_; } /** * Get a ptr to the underlying GObject * * @return GObject or NULL */ GObject* object() const { return self_; } /** * Unref the object * */ void unref() noexcept { g_object_unref(self_); } /** * Ref the object * */ void ref() noexcept { g_object_ref(self_); } private: mutable GObject *self_{}; }; /** * Thin wrapper around a GMimeContentType * */ struct MimeContentType: public Object { MimeContentType(GMimeContentType *ctype) : Object{G_OBJECT(ctype)} { if (!GMIME_IS_CONTENT_TYPE(self())) throw std::runtime_error("not a content-type"); } std::string media_type() const noexcept { return g_mime_content_type_get_media_type(self()); } std::string media_subtype() const noexcept { return g_mime_content_type_get_media_subtype(self()); } Option<std::string> mime_type() const noexcept { return to_string_opt_gchar(g_mime_content_type_get_mime_type(self())); } bool is_type(const std::string& type, const std::string& subtype) const { return g_mime_content_type_is_type(self(), type.c_str(), subtype.c_str()); } private: GMimeContentType* self() const { return reinterpret_cast<GMimeContentType*>(object()); } }; /** * Thin wrapper around a GMimeStream * */ struct MimeStream: public Object { ssize_t write(const char* buf, ::size_t size) { return g_mime_stream_write(self(), buf, size); } bool reset() { return g_mime_stream_reset(self()) < 0 ? false : true; } bool flush() { return g_mime_stream_flush(self()) < 0 ? false : true; } static MimeStream make_mem() { MimeStream mstream{g_mime_stream_mem_new()}; mstream.unref(); /* remove extra ref */ return mstream; } static MimeStream make_filtered(MimeStream& stream) { MimeStream mstream{g_mime_stream_filter_new(stream.self())}; mstream.unref(); /* remove extra refs */ return mstream; } static MimeStream make_from_stream(GMimeStream *strm) { MimeStream mstream{strm}; mstream.unref(); /* remove extra ref */ return mstream; } private: MimeStream(GMimeStream *stream): Object(G_OBJECT(stream)) { if (!GMIME_IS_STREAM(self())) throw std::runtime_error("not a mime-stream"); }; GMimeStream* self() const { return reinterpret_cast<GMimeStream*>(object()); } }; template<typename S, typename T> constexpr Option<std::string_view> to_string_view_opt(const S& seq, T t) { auto&& it = seq_find_if(seq, [&](auto&& item){return item.first == t;}); if (it == seq.cend()) return Nothing; else return it->second; } /** * Thin wrapper around a GMimeDataWrapper * */ struct MimeDataWrapper: public Object { MimeDataWrapper(GMimeDataWrapper *wrapper): Object(G_OBJECT(wrapper)) { if (!GMIME_IS_DATA_WRAPPER(self())) throw std::runtime_error("not a data-wrapper"); }; Result<size_t> write_to_stream(MimeStream& stream) const { if (auto&& res = g_mime_data_wrapper_write_to_stream( self(), GMIME_STREAM(stream.object())) ; res < 0) return Err(Error::Code::Message, "failed to write to stream"); else return Ok(static_cast<size_t>(res)); } private: GMimeDataWrapper* self() const { return reinterpret_cast<GMimeDataWrapper*>(object()); } }; /** * Thin wrapper around a GMimeCertifcate * */ struct MimeCertificate: public Object { MimeCertificate(GMimeCertificate *cert) : Object{G_OBJECT(cert)} { if (!GMIME_IS_CERTIFICATE(self())) throw std::runtime_error("not a certificate"); } enum struct PubkeyAlgo { Default = GMIME_PUBKEY_ALGO_DEFAULT, Rsa = GMIME_PUBKEY_ALGO_RSA, RsaE = GMIME_PUBKEY_ALGO_RSA_E, RsaS = GMIME_PUBKEY_ALGO_RSA_S, ElgE = GMIME_PUBKEY_ALGO_ELG_E, Dsa = GMIME_PUBKEY_ALGO_DSA, Ecc = GMIME_PUBKEY_ALGO_ECC, Elg = GMIME_PUBKEY_ALGO_ELG, EcDsa = GMIME_PUBKEY_ALGO_ECDSA, EcDh = GMIME_PUBKEY_ALGO_ECDH, EdDsa = GMIME_PUBKEY_ALGO_EDDSA, }; enum struct DigestAlgo { Default = GMIME_DIGEST_ALGO_DEFAULT, Md5 = GMIME_DIGEST_ALGO_MD5, Sha1 = GMIME_DIGEST_ALGO_SHA1, RipEmd160 = GMIME_DIGEST_ALGO_RIPEMD160, Md2 = GMIME_DIGEST_ALGO_MD2, Tiger192 = GMIME_DIGEST_ALGO_TIGER192, Haval5160 = GMIME_DIGEST_ALGO_HAVAL5160, Sha256 = GMIME_DIGEST_ALGO_SHA256, Sha384 = GMIME_DIGEST_ALGO_SHA384, Sha512 = GMIME_DIGEST_ALGO_SHA512, Sha224 = GMIME_DIGEST_ALGO_SHA224, Md4 = GMIME_DIGEST_ALGO_MD4, Crc32 = GMIME_DIGEST_ALGO_CRC32, Crc32Rfc1510 = GMIME_DIGEST_ALGO_CRC32_RFC1510, Crc32Rfc2440 = GMIME_DIGEST_ALGO_CRC32_RFC2440, }; enum struct Trust { Unknown = GMIME_TRUST_UNKNOWN, Undefined = GMIME_TRUST_UNDEFINED, Never = GMIME_TRUST_NEVER, Marginal = GMIME_TRUST_MARGINAL, TrustFull = GMIME_TRUST_FULL, TrustUltimate = GMIME_TRUST_ULTIMATE, }; enum struct Validity { Unknown = GMIME_VALIDITY_UNKNOWN, Undefined = GMIME_VALIDITY_UNDEFINED, Never = GMIME_VALIDITY_NEVER, Marginal = GMIME_VALIDITY_MARGINAL, Full = GMIME_VALIDITY_FULL, Ultimate = GMIME_VALIDITY_ULTIMATE, }; PubkeyAlgo pubkey_algo() const { return static_cast<PubkeyAlgo>( g_mime_certificate_get_pubkey_algo(self())); } DigestAlgo digest_algo() const { return static_cast<DigestAlgo>( g_mime_certificate_get_digest_algo(self())); } Validity id_validity() const { return static_cast<Validity>( g_mime_certificate_get_id_validity(self())); } Trust trust() const { return static_cast<Trust>( g_mime_certificate_get_trust(self())); } Option<std::string> issuer_serial() const { return to_string_opt(g_mime_certificate_get_issuer_serial(self())); } Option<std::string> issuer_name() const { return to_string_opt(g_mime_certificate_get_issuer_name(self())); } Option<std::string> fingerprint() const { return to_string_opt(g_mime_certificate_get_fingerprint(self())); } Option<std::string> key_id() const { return to_string_opt(g_mime_certificate_get_key_id(self())); } Option<std::string> name() const { return to_string_opt(g_mime_certificate_get_name(self())); } Option<std::string> user_id() const { return to_string_opt(g_mime_certificate_get_user_id(self())); } Option<::time_t> created() const { if (auto t = g_mime_certificate_get_created(self()); t >= 0) return t; else return Nothing; } Option<::time_t> expires() const { if (auto t = g_mime_certificate_get_expires(self()); t >= 0) return t; else return Nothing; } private: GMimeCertificate* self() const { return reinterpret_cast<GMimeCertificate*>(object()); } }; constexpr std::array<std::pair<MimeCertificate::PubkeyAlgo, std::string_view>, 11> AllPubkeyAlgos = {{ { MimeCertificate::PubkeyAlgo::Default, "default"}, { MimeCertificate::PubkeyAlgo::Rsa, "rsa"}, { MimeCertificate::PubkeyAlgo::RsaE, "rsa-encryption-only"}, { MimeCertificate::PubkeyAlgo::RsaS, "rsa-signing-only"}, { MimeCertificate::PubkeyAlgo::ElgE, "el-gamal-encryption-only"}, { MimeCertificate::PubkeyAlgo::Dsa, "dsa"}, { MimeCertificate::PubkeyAlgo::Ecc, "elliptic curve"}, { MimeCertificate::PubkeyAlgo::Elg, "el-gamal"}, { MimeCertificate::PubkeyAlgo::EcDsa, "elliptic-curve+dsa"}, { MimeCertificate::PubkeyAlgo::EcDh, "elliptic-curve+diffie-helman"}, { MimeCertificate::PubkeyAlgo::EdDsa, "elliptic-curve+dsa-2"} }}; constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::PubkeyAlgo algo) { return to_string_view_opt(AllPubkeyAlgos, algo); } constexpr std::array<std::pair<MimeCertificate::DigestAlgo, std::string_view>, 15> AllDigestAlgos = {{ { MimeCertificate::DigestAlgo::Default, "default"}, { MimeCertificate::DigestAlgo::Md5, "md5"}, { MimeCertificate::DigestAlgo::Sha1, "sha1"}, { MimeCertificate::DigestAlgo::RipEmd160, "ripemd-160"}, { MimeCertificate::DigestAlgo::Md2, "md2"}, { MimeCertificate::DigestAlgo::Tiger192, "tiger-192"}, { MimeCertificate::DigestAlgo::Haval5160, "haval-5-160"}, { MimeCertificate::DigestAlgo::Sha256, "sha-256"}, { MimeCertificate::DigestAlgo::Sha384, "sha-384"}, { MimeCertificate::DigestAlgo::Sha512, "sha-512"}, { MimeCertificate::DigestAlgo::Sha224, "sha-224"}, { MimeCertificate::DigestAlgo::Md4, "md4"}, { MimeCertificate::DigestAlgo::Crc32, "crc32"}, { MimeCertificate::DigestAlgo::Crc32Rfc1510, "crc32-rfc1510"}, { MimeCertificate::DigestAlgo::Crc32Rfc2440, "crc32-rfc2440"}, }}; constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::DigestAlgo algo) { return to_string_view_opt(AllDigestAlgos, algo); } constexpr std::array<std::pair<MimeCertificate::Trust, std::string_view>, 6> AllTrusts = {{ { MimeCertificate::Trust::Unknown, "unknown" }, { MimeCertificate::Trust::Undefined, "undefined" }, { MimeCertificate::Trust::Never, "never" }, { MimeCertificate::Trust::Marginal, "marginal" }, { MimeCertificate::Trust::TrustFull, "trust-full" }, { MimeCertificate::Trust::TrustUltimate,"trust-ultimate" }, }}; constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Trust trust) { return to_string_view_opt(AllTrusts, trust); } constexpr std::array<std::pair<MimeCertificate::Validity, std::string_view>, 6> AllValidities = {{ { MimeCertificate::Validity::Unknown, "unknown" }, { MimeCertificate::Validity::Undefined, "undefined" }, { MimeCertificate::Validity::Never, "never" }, { MimeCertificate::Validity::Marginal, "marginal" }, { MimeCertificate::Validity::Full, "full" }, { MimeCertificate::Validity::Ultimate, "ultimate" }, }}; constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Validity val) { return to_string_view_opt(AllValidities, val); } /** * Thin wrapper around a GMimeSignature * */ struct MimeSignature: public Object { MimeSignature(GMimeSignature *sig) : Object{G_OBJECT(sig)} { if (!GMIME_IS_SIGNATURE(self())) throw std::runtime_error("not a signature"); } /** * Signature status * */ enum struct Status { Valid = GMIME_SIGNATURE_STATUS_VALID, Green = GMIME_SIGNATURE_STATUS_GREEN, Red = GMIME_SIGNATURE_STATUS_RED, KeyRevoked = GMIME_SIGNATURE_STATUS_KEY_REVOKED, KeyExpired = GMIME_SIGNATURE_STATUS_KEY_EXPIRED, SigExpired = GMIME_SIGNATURE_STATUS_SIG_EXPIRED, KeyMissing = GMIME_SIGNATURE_STATUS_KEY_MISSING, CrlMissing = GMIME_SIGNATURE_STATUS_CRL_MISSING, CrlTooOld = GMIME_SIGNATURE_STATUS_CRL_TOO_OLD, BadPolicy = GMIME_SIGNATURE_STATUS_BAD_POLICY, SysError = GMIME_SIGNATURE_STATUS_SYS_ERROR, TofuConflict = GMIME_SIGNATURE_STATUS_TOFU_CONFLICT }; Status status() const { return static_cast<Status>( g_mime_signature_get_status(self())); } ::time_t created() const { return g_mime_signature_get_created(self()); } ::time_t expires() const { return g_mime_signature_get_expires(self()); } const MimeCertificate certificate() const { return MimeCertificate{g_mime_signature_get_certificate(self())}; } private: GMimeSignature* self() const { return reinterpret_cast<GMimeSignature*>(object()); } }; constexpr std::array<std::pair<MimeSignature::Status, std::string_view>, 12> AllMimeSignatureStatuses= {{ { MimeSignature::Status::Valid, "valid" }, { MimeSignature::Status::Green, "green" }, { MimeSignature::Status::Red, "red" }, { MimeSignature::Status::KeyRevoked, "key-revoked" }, { MimeSignature::Status::KeyExpired, "key-expired" }, { MimeSignature::Status::SigExpired, "sig-expired" }, { MimeSignature::Status::KeyMissing, "key-missing" }, { MimeSignature::Status::CrlMissing, "crl-missing" }, { MimeSignature::Status::CrlTooOld, "crl-too-old" }, { MimeSignature::Status::BadPolicy, "bad-policy" }, { MimeSignature::Status::SysError, "sys-error" }, { MimeSignature::Status::TofuConflict, "tofu-confict" }, }}; MU_ENABLE_BITOPS(MimeSignature::Status); static inline std::string to_string(MimeSignature::Status status) { std::string str; for (auto&& item: AllMimeSignatureStatuses) { if (none_of(item.first & status)) continue; if (!str.empty()) str += ", "; str += item.second; } if (str.empty()) str = "none"; return str; } /** * Thin wrapper around a GMimeDecryptResult * */ struct MimeDecryptResult: public Object { MimeDecryptResult (GMimeDecryptResult *decres) : Object{G_OBJECT(decres)} { if (!GMIME_IS_DECRYPT_RESULT(self())) throw std::runtime_error("not a decrypt-result"); } std::vector<MimeCertificate> recipients() const noexcept; std::vector<MimeSignature> signatures() const noexcept; enum struct CipherAlgo { Default = GMIME_CIPHER_ALGO_DEFAULT, Idea = GMIME_CIPHER_ALGO_IDEA, Des3 = GMIME_CIPHER_ALGO_3DES, Cast5 = GMIME_CIPHER_ALGO_CAST5, Blowfish = GMIME_CIPHER_ALGO_BLOWFISH, Aes = GMIME_CIPHER_ALGO_AES, Aes192 = GMIME_CIPHER_ALGO_AES192, Aes256 = GMIME_CIPHER_ALGO_AES256, TwoFish = GMIME_CIPHER_ALGO_TWOFISH, Camellia128 = GMIME_CIPHER_ALGO_CAMELLIA128, Camellia192 = GMIME_CIPHER_ALGO_CAMELLIA192, Camellia256 = GMIME_CIPHER_ALGO_CAMELLIA256 }; CipherAlgo cipher() const noexcept { return static_cast<CipherAlgo>( g_mime_decrypt_result_get_cipher(self())); } using DigestAlgo = MimeCertificate::DigestAlgo; DigestAlgo mdc() const noexcept { return static_cast<DigestAlgo>( g_mime_decrypt_result_get_mdc(self())); } Option<std::string> session_key() const noexcept { return to_string_opt(g_mime_decrypt_result_get_session_key(self())); } private: GMimeDecryptResult* self() const { return reinterpret_cast<GMimeDecryptResult*>(object()); } }; constexpr std::array<std::pair<MimeDecryptResult::CipherAlgo, std::string_view>, 12> AllCipherAlgos= {{ {MimeDecryptResult::CipherAlgo::Default, "default"}, {MimeDecryptResult::CipherAlgo::Idea, "idea"}, {MimeDecryptResult::CipherAlgo::Des3, "3des"}, {MimeDecryptResult::CipherAlgo::Cast5, "cast5"}, {MimeDecryptResult::CipherAlgo::Blowfish, "blowfish"}, {MimeDecryptResult::CipherAlgo::Aes, "aes"}, {MimeDecryptResult::CipherAlgo::Aes192, "aes192"}, {MimeDecryptResult::CipherAlgo::Aes256, "aes256"}, {MimeDecryptResult::CipherAlgo::TwoFish, "twofish"}, {MimeDecryptResult::CipherAlgo::Camellia128, "camellia128"}, {MimeDecryptResult::CipherAlgo::Camellia192, "camellia192"}, {MimeDecryptResult::CipherAlgo::Camellia256, "camellia256"}, }}; constexpr Option<std::string_view> to_string_view_opt(MimeDecryptResult::CipherAlgo algo) { return to_string_view_opt(AllCipherAlgos, algo); } /** * Thin wrapper around a GMimeCryptoContext * */ struct MimeCryptoContext : public Object { /** * Make a new PGP crypto context. * * For 'test-mode', pass a test-path; in this mode GPG will be setup * in an isolated mode so it does not affect normal usage. * * @param testpath (for unit-tests) pass a path to an existing dir to * create a pgp setup. For normal use, leave empty. * * @return A MimeCryptoContext or an error */ static Result<MimeCryptoContext> make_gpg(const std::string& testpath={}) try { if (!testpath.empty()) { if (auto&& res = setup_gpg_test(testpath); !res) return Err(res.error()); } MimeCryptoContext ctx(g_mime_gpg_context_new()); ctx.unref(); /* remove extra ref */ return Ok(std::move(ctx)); } catch (...) { return Err(Error::Code::Crypto, "failed to create crypto context"); } static Result<MimeCryptoContext> make(const std::string& protocol) { auto ctx = g_mime_crypto_context_new(protocol.c_str()); if (!ctx) return Err(Error::Code::Crypto, "unsupported protocol " + protocol); MimeCryptoContext mctx{ctx}; mctx.unref(); /* remove extra ref */ return Ok(std::move(mctx)); } Option<std::string> encryption_protocol() const noexcept { return to_string_opt(g_mime_crypto_context_get_encryption_protocol(self())); } Option<std::string> signature_protocol() const noexcept { return to_string_opt(g_mime_crypto_context_get_signature_protocol(self())); } Option<std::string> key_exchange_protocol() const noexcept { return to_string_opt(g_mime_crypto_context_get_key_exchange_protocol(self())); } /** * Imports a stream of keys/certificates contained within stream into * the key/certificate database controlled by @this. * * @param stream * * @return number of keys imported, or an error. */ Result<size_t> import_keys(MimeStream& stream); /** * Prototype for a request-password function. * * @param ctx the MimeCryptoContext making the request * @param user_id the user_id of the password being requested * @param prompt a string containing some helpful context for the prompt * @param reprompt true if this password request is a reprompt due to a * previously bad password response * @param response a stream for the application to write the password to * (followed by a newline '\n' character) * * @return nothing (Ok) or an error, */ using PasswordRequestFunc = std::function<Result<void>( const MimeCryptoContext& ctx, const std::string& user_id, const std::string& prompt, bool reprompt, MimeStream& response)>; /** * Set a function to request a password. * * @param pw_func password function. */ void set_request_password(PasswordRequestFunc pw_func); private: MimeCryptoContext(GMimeCryptoContext *ctx): Object{G_OBJECT(ctx)} { if (!GMIME_IS_CRYPTO_CONTEXT(self())) throw std::runtime_error("not a crypto-context"); } static Result<void> setup_gpg_test(const std::string& testpath); GMimeCryptoContext* self() const { return reinterpret_cast<GMimeCryptoContext*>(object()); } }; /** * Thin wrapper around a GMimeObject * */ class MimeObject: public Object { public: /** * Construct a new MimeObject. Take a ref on the obj * * @param mime_part mime-part pointer */ MimeObject(const Object& obj): Object{obj} { if (!GMIME_IS_OBJECT(self())) throw std::runtime_error("not a mime-object"); } MimeObject(GMimeObject *mobj): Object{G_OBJECT(mobj)} { if (mobj && !GMIME_IS_OBJECT(self())) throw std::runtime_error("not a mime-object"); } /** * Get a header from the MimeObject * * @param header the header to retrieve * * @return header value (UTF-8) or Nothing */ Option<std::string> header(const std::string& header) const noexcept; /** * Get all headers as pairs of name, value * * @return all headers */ std::vector<std::pair<std::string, std::string>> headers() const noexcept; /** * Get the content type * * @return the content-type or Nothing */ Option<MimeContentType> content_type() const noexcept { auto ct{g_mime_object_get_content_type(self())}; if (!ct) return Nothing; else return MimeContentType(ct); } Option<std::string> mime_type() const noexcept { if (auto ct = content_type(); !ct) return Nothing; else return ct->mime_type(); } /** * Get the content-type parameter * * @param param name of parameter * * @return the value of the parameter, or Nothing */ Option<std::string> content_type_parameter(const std::string& param) const noexcept { return Mu::to_string_opt( g_mime_object_get_content_type_parameter(self(), param.c_str())); } /** * Write this MimeObject to some stream * * @param f_opts formatting options * @param stream the stream * * @return the number or bytes written or an error */ Result<size_t> write_to_stream(const MimeFormatOptions& f_opts, MimeStream& stream) const; /** * Write the object to a string. * * @return */ Option<std::string> to_string_opt() const noexcept; /** * Write object to a file * * @param path path to file * @param overwrite if true, overwrite existing file, if it bqexists * * @return size of the wrtten file, or an error. */ Result<size_t> to_file(const std::string& path, bool overwrite) const noexcept; /* * subtypes. */ /** * Is this a MimePart? * * @return true or false */ bool is_part() const { return GMIME_IS_PART(self()); } /** * Is this a MimeMultiPart? * * @return true or false */ bool is_multipart() const { return GMIME_IS_MULTIPART(self());} /** * Is this a MimeMultiPart? * * @return true or false */ bool is_multipart_encrypted() const { return GMIME_IS_MULTIPART_ENCRYPTED(self()); } /** * Is this a MimeMultiPart? * * @return true or false */ bool is_multipart_signed() const { return GMIME_IS_MULTIPART_SIGNED(self()); } /** * Is this a MimeMessage? * * @return true or false */ bool is_message() const { return GMIME_IS_MESSAGE(self());} /** * Is this a MimeMessagePart? * * @return true orf alse */ bool is_message_part() const { return GMIME_IS_MESSAGE_PART(self());} /** * Is this a MimeApplicationpkcs7Mime? * * @return true orf alse */ bool is_mime_application_pkcs7_mime() const { return GMIME_IS_APPLICATION_PKCS7_MIME(self()); } /** * Callback for for_each(). See GMimeObjectForEachFunc. * */ using ForEachFunc = std::function<void(const MimeObject& parent, const MimeObject& part)>; private: GMimeObject* self() const { return reinterpret_cast<GMimeObject*>(object()); } }; /** * Thin wrapper around a GMimeMessage * */ class MimeMessage: public MimeObject { public: /** * Construct a MimeMessage * * @param obj an Object of the right type */ MimeMessage(const Object& obj): MimeObject(obj) { if (!is_message()) throw std::runtime_error("not a mime-message"); } /** * Make a MimeMessage from a file * * @param path path to the file * * @return a MimeMessage or an error. */ static Result<MimeMessage> make_from_file (const std::string& path); /** * Make a MimeMessage from a string * * @param path path to the file * * @return a MimeMessage or an error. */ static Result<MimeMessage> make_from_text (const std::string& text); /** * Get the contacts of a given type, or None for _all_ * * @param ctype contact type * * @return contacts */ Contacts contacts(Contact::Type ctype) const noexcept; /** * Gets the message-id if it exists, or nullopt otherwise. * * @return string or nullopt */ Option<std::string> message_id() const noexcept { return Mu::to_string_opt(g_mime_message_get_message_id(self())); } /** * Gets the message-id if it exists, or nullopt otherwise. * * @return string or nullopt */ Option<std::string> subject() const noexcept { return Mu::to_string_opt(g_mime_message_get_subject(self())); } /** * Gets the date if it exists, or nullopt otherwise. * * @return a time_t value (expressed as a 64-bit number) or nullopt */ Option<int64_t> date() const noexcept; /** * Get the references for this message (including in-reply-to), in the * order of older..newer; the first one would the oldest parent, and * in-reply-to would be the last one (if any). These are de-duplicated, * and known-fake references removed (see implementation) * * @return references. */ std::vector<std::string> references() const noexcept; /** * Recursively apply func tol all parts of this message * * @param func a function */ void for_each(const ForEachFunc& func) const noexcept; private: GMimeMessage* self() const { return reinterpret_cast<GMimeMessage*>(object()); } }; /** * Thin wrapper around a GMimePart. * */ class MimePart: public MimeObject { public: /** * Construct a MimePart * * @param obj an Object of the right type */ MimePart(const Object& obj): MimeObject(obj) { if (!is_part()) throw std::runtime_error("not a mime-part"); } /** * Determines whether or not the part is an attachment based on the * value of the Content-Disposition header. * * @return true or false */ bool is_attachment() const noexcept { return g_mime_part_is_attachment(self()); } /** * Gets the value of the Content-Description for this mime part * if it exists, or nullopt otherwise. * * @return string or nullopt */ Option<std::string> content_description() const noexcept { return Mu::to_string_opt(g_mime_part_get_content_description(self())); } /** * Gets the value of the Content-Id for this mime part * if it exists, or nullopt otherwise. * * @return string or nullopt */ Option<std::string> content_id() const noexcept { return Mu::to_string_opt(g_mime_part_get_content_id(self())); } /** * Gets the value of the Content-Md5 header for this mime part * if it exists, or nullopt otherwise. * * @return string or nullopt */ Option<std::string> content_md5() const noexcept { return Mu::to_string_opt(g_mime_part_get_content_md5(self())); } /** * Verify the content md5 for the specified mime part. Returns false if * the mime part does not contain a Content-MD5. * * @return true or false */ bool verify_content_md5() const noexcept { return g_mime_part_verify_content_md5(self()); } /** * Gets the value of the Content-Location for this mime part if it * exists, or nullopt otherwise. * * @return string or nullopt */ Option<std::string> content_location() const noexcept { return Mu::to_string_opt(g_mime_part_get_content_location(self())); } MimeDataWrapper content() const noexcept { return MimeDataWrapper{g_mime_part_get_content(self())}; } /** * Gets the filename for this mime part if it exists, or nullopt * otherwise. * * @return string or nullopt */ Option<std::string> filename() const noexcept { return Mu::to_string_opt(g_mime_part_get_filename(self())); } /** * Size of content, in bytes * * @return size */ size_t size() const noexcept; /** * Get as UTF-8 string * * @return a string, or NULL. */ Option<std::string> to_string() const noexcept; /** * Write part to a file * * @param path path to file * @param overwrite if true, overwrite existing file, if it bqexists * * @return size of the wrtten file, or an error. */ Result<size_t> to_file(const std::string& path, bool overwrite) const noexcept; /** * Types of Content Encoding. * */ enum struct ContentEncoding { Default = GMIME_CONTENT_ENCODING_DEFAULT, SevenBit = GMIME_CONTENT_ENCODING_7BIT, EightBit = GMIME_CONTENT_ENCODING_8BIT, Binary = GMIME_CONTENT_ENCODING_BINARY, Base64 = GMIME_CONTENT_ENCODING_BASE64, QuotedPrintable = GMIME_CONTENT_ENCODING_QUOTEDPRINTABLE, UuEncode = GMIME_CONTENT_ENCODING_UUENCODE }; /** * Gets the content encoding of the mime part. * * @return the content encoding */ ContentEncoding content_encoding() const noexcept { const auto enc{g_mime_part_get_content_encoding(self())}; g_return_val_if_fail(enc <= GMIME_CONTENT_ENCODING_UUENCODE, ContentEncoding::Default); return static_cast<ContentEncoding>(enc); } /** * Types of OpenPGP data * */ enum struct OpenPGPData { None = GMIME_OPENPGP_DATA_NONE, Encrypted = GMIME_OPENPGP_DATA_ENCRYPTED, Signed = GMIME_OPENPGP_DATA_SIGNED, PublicKey = GMIME_OPENPGP_DATA_PUBLIC_KEY, PrivateKey = GMIME_OPENPGP_DATA_PRIVATE_KEY, }; /** * Gets whether or not (and what type) of OpenPGP data is contained * * @return OpenGPGData */ OpenPGPData openpgp_data() const noexcept { const auto data{g_mime_part_get_openpgp_data(self())}; g_return_val_if_fail(data <= GMIME_OPENPGP_DATA_PRIVATE_KEY, OpenPGPData::None); return static_cast<OpenPGPData>(data); } private: GMimePart* self() const { return reinterpret_cast<GMimePart*>(object()); } }; /** * Thin wrapper around a GMimeMessagePart. * */ class MimeMessagePart: public MimeObject { public: /** * Construct a MimeMessagePart * * @param obj an Object of the right type */ MimeMessagePart(const Object& obj): MimeObject(obj) { if (!is_message_part()) throw std::runtime_error("not a mime-message-part"); } /** * Get the MimeMessage for this MimeMessagePart. * * @return the MimeMessage or Nothing */ Option<MimeMessage> get_message() const { auto msg{g_mime_message_part_get_message(self())}; if (msg) return MimeMessage(Object(G_OBJECT(msg))); else return Nothing; } private: GMimeMessagePart* self() const { return reinterpret_cast<GMimeMessagePart*>(object()); } }; /** * Thin wrapper around a GMimeApplicationPkcs7Mime * */ class MimeApplicationPkcs7Mime: public MimePart { public: /** * Construct a MimeApplicationPkcs7Mime * * @param obj an Object of the right type */ MimeApplicationPkcs7Mime(const Object& obj): MimePart(obj) { if (!is_mime_application_pkcs7_mime()) throw std::runtime_error("not a mime-application-pkcs7-mime"); } enum struct SecureMimeType { CompressedData = GMIME_SECURE_MIME_TYPE_COMPRESSED_DATA, EnvelopedData = GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA, SignedData = GMIME_SECURE_MIME_TYPE_SIGNED_DATA, CertsOnly = GMIME_SECURE_MIME_TYPE_CERTS_ONLY, Unknown = GMIME_SECURE_MIME_TYPE_UNKNOWN }; SecureMimeType smime_type() const { return static_cast<SecureMimeType>( g_mime_application_pkcs7_mime_get_smime_type(self())); } private: GMimeApplicationPkcs7Mime* self() const { return reinterpret_cast<GMimeApplicationPkcs7Mime*>(object()); } }; /** * Thin wrapper around a GMimeMultiPart * */ class MimeMultipart: public MimeObject { public: /** * Construct a MimeMultipart * * @param obj an Object of the right type */ MimeMultipart(const Object& obj): MimeObject(obj) { if (!is_multipart()) throw std::runtime_error("not a mime-multipart"); } Option<MimePart> signed_content_part() const { return part(GMIME_MULTIPART_SIGNED_CONTENT); } Option<MimePart> signed_signature_part() const { return part(GMIME_MULTIPART_SIGNED_SIGNATURE); } Option<MimePart> encrypted_version_part() const { return part(GMIME_MULTIPART_ENCRYPTED_VERSION); } Option<MimePart> encrypted_content_part() const { return part(GMIME_MULTIPART_ENCRYPTED_CONTENT); } /** * Recursively apply func to all parts * * @param func a function */ void for_each(const ForEachFunc& func) const noexcept; private: // Note: the part may not be available if the message was marked as // _signed_ or _encrypted_ because it contained a forwarded signed or // encrypted message. Option<MimePart> part(int index) const { if (auto&& p{g_mime_multipart_get_part(self() ,index)}; !GMIME_IS_PART(p)) return Nothing; else return Some(MimeObject{p}); } GMimeMultipart* self() const { return reinterpret_cast<GMimeMultipart*>(object()); } }; /** * Thin wrapper around a GMimeMultiPartEncrypted * */ class MimeMultipartEncrypted: public MimeMultipart { public: /** * Construct a MimeMultipartEncrypted * * @param obj an Object of the right type */ MimeMultipartEncrypted(const Object& obj): MimeMultipart(obj) { if (!is_multipart_encrypted()) throw std::runtime_error("not a mime-multipart-encrypted"); } enum struct DecryptFlags { None = GMIME_DECRYPT_NONE, ExportSessionKey = GMIME_DECRYPT_EXPORT_SESSION_KEY, NoVerify = GMIME_DECRYPT_NO_VERIFY, EnableKeyserverLookups = GMIME_DECRYPT_ENABLE_KEYSERVER_LOOKUPS, EnableOnlineCertificateChecks = GMIME_DECRYPT_ENABLE_ONLINE_CERTIFICATE_CHECKS }; using Decrypted = std::pair<MimeObject, MimeDecryptResult>; Result<Decrypted> decrypt(const MimeCryptoContext& ctx, DecryptFlags flags=DecryptFlags::None, const std::string& session_key = {}) const noexcept; private: GMimeMultipartEncrypted* self() const { return reinterpret_cast<GMimeMultipartEncrypted*>(object()); } }; MU_ENABLE_BITOPS(MimeMultipartEncrypted::DecryptFlags); /** * Thin wrapper around a GMimeMultiPartSigned * */ class MimeMultipartSigned: public MimeMultipart { public: /** * Construct a MimeMultipartSigned * * @param obj an Object of the right type */ MimeMultipartSigned(const Object& obj): MimeMultipart(obj) { if (!is_multipart_signed()) throw std::runtime_error("not a mime-multipart-signed"); } enum struct VerifyFlags { None = GMIME_VERIFY_NONE, EnableKeyserverLookups = GMIME_VERIFY_ENABLE_KEYSERVER_LOOKUPS, EnableOnlineCertificateChecks = GMIME_VERIFY_ENABLE_ONLINE_CERTIFICATE_CHECKS }; // Result<std::vector<MimeSignature>> verify(VerifyFlags vflags=VerifyFlags::None) const noexcept; Result<std::vector<MimeSignature>> verify(const MimeCryptoContext& ctx, VerifyFlags vflags=VerifyFlags::None) const noexcept; private: GMimeMultipartSigned* self() const { return reinterpret_cast<GMimeMultipartSigned*>(object()); } }; MU_ENABLE_BITOPS(MimeMultipartSigned::VerifyFlags); } // namespace Mu #endif /* MU_MIME_OBJECT_HH__ */ ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-priority.cc����������������������������������������������������������������0000664�0000000�0000000�00000004166�14523230065�0017232�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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-priority.hh" using namespace Mu; std::string Mu::to_string(Priority prio) { return std::string{priority_name(prio)}; } /* * tests... also build as runtime-tests, so we can get coverage info */ #ifdef BUILD_TESTS #include <glib.h> #define static_assert g_assert_true #endif /*BUILD_TESTS*/ [[maybe_unused]] static void test_priority_to_char() { static_assert(to_char(Priority::Low) == 'l'); static_assert(to_char(Priority::Normal) == 'n'); static_assert(to_char(Priority::High) == 'h'); } [[maybe_unused]] static void test_priority_from_char() { static_assert(priority_from_char('l') == Priority::Low); static_assert(priority_from_char('n') == Priority::Normal); static_assert(priority_from_char('h') == Priority::High); static_assert(priority_from_char('x') == Priority::Normal); } [[maybe_unused]] static void test_priority_name() { static_assert(priority_name(Priority::Low) == "low"); static_assert(priority_name(Priority::Normal) == "normal"); static_assert(priority_name(Priority::High) == "high"); } #ifdef BUILD_TESTS int main(int argc, char* argv[]) { g_test_init(&argc, &argv, NULL); g_test_add_func("/message/priority/to-char", test_priority_to_char); g_test_add_func("/message/priority/from-char", test_priority_from_char); g_test_add_func("/message/priority/name", test_priority_name); return g_test_run(); } #endif /*BUILD_TESTS*/ ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/message/mu-priority.hh����������������������������������������������������������������0000664�0000000�0000000�00000004741�14523230065�0017243�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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_PRIORITY_HH__ #define MU_PRIORITY_HH__ #include <array> #include <string> #include <string_view> #include "mu-fields.hh" namespace Mu { /** * Message priorities * */ /** * The priority ids * */ enum struct Priority : char { Low = 'l', /**< Low priority */ Normal = 'n', /**< Normal priority */ High = 'h', /**< High priority */ }; /** * Sequence of all message priorities. */ static constexpr std::array<Priority, 3> AllMessagePriorities = { Priority::Low, Priority::Normal, Priority::High}; /** * Get the char for some priority * * @param id an id * * @return the char */ constexpr char to_char(Priority prio) { return static_cast<char>(prio); } /** * Get the priority for some character; unknown onws * become Normal. * * @param c some character */ constexpr Priority priority_from_char(char c) { switch (c) { case 'l': return Priority::Low; case 'h': return Priority::High; case 'n': default: return Priority::Normal; } } /** * Get the name for a given priority * * @return the name */ constexpr std::string_view priority_name(Priority prio) { switch (prio) { case Priority::Low: return "low"; case Priority::High: return "high"; case Priority::Normal: default: return "normal"; } } /** * Get the name for a given priority (backward compatibility) * * @return the name */ constexpr const char* priority_name_c_str(Priority prio) { switch (prio) { case Priority::Low: return "low"; case Priority::High: return "high"; case Priority::Normal: default: return "normal"; } } /** * Get a the message priority as a string * * @param prio priority * * @return a string */ std::string to_string(Priority prio); } // namespace Mu #endif /*MU_PRIORITY_HH_*/ �������������������������������mu-1.10.8/lib/message/test-mu-message.cc������������������������������������������������������������0000664�0000000�0000000�00000120664�14523230065�0017754�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2023 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 "utils/mu-test-utils.hh" #include "mu-message.hh" #include "mu-mime-object.hh" #include <glib.h> #include <regex> using namespace Mu; /* * test message 1 */ static void test_message_mailing_list() { constexpr const char *test_message_1 = R"(Return-Path: <sqlite-dev-bounces@sqlite.org> 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) 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 Precedence: list Reply-To: sqlite-dev@sqlite.org List-Id: <sqlite-dev.sqlite.org> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Sender: 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]; )"; auto message{Message::make_from_text( test_message_1, "/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S")}; g_assert_true(!!message); assert_equal(message->path(), "/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S"); g_assert_true(message->maildir().empty()); g_assert_true(message->bcc().empty()); g_assert_true(!message->body_html()); assert_equal(message->body_text().value_or(""), R"(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]; )"); g_assert_true(message->cc().empty()); g_assert_cmpuint(message->date(), ==, 1217842849); g_assert_true(message->flags() == (Flags::MailingList | Flags::Seen)); const auto from{message->from()}; g_assert_cmpuint(from.size(),==,1); assert_equal(from.at(0).name, ""); assert_equal(from.at(0).email, "anon@example.com"); assert_equal(message->mailing_list(), "sqlite-dev.sqlite.org"); assert_equal(message->message_id(), "83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net"); g_assert_true(message->priority() == Priority::Low); g_assert_cmpuint(message->size(),==,::strlen(test_message_1)); /* text-based message use time({}) as their changed-time */ g_assert_cmpuint(::time({}) - message->changed(), >=, 0); g_assert_cmpuint(::time({}) - message->changed(), <=, 2); g_assert_true(message->references().empty()); assert_equal(message->subject(), "[sqlite-dev] VM optimization inside sqlite3VdbeExec"); const auto to{message->to()}; g_assert_cmpuint(to.size(),==,1); assert_equal(to.at(0).name, ""); assert_equal(to.at(0).email, "sqlite-dev@sqlite.org"); assert_equal(message->header("X-Mailer").value_or(""), "Apple Mail (2.926)"); auto all_contacts{message->all_contacts()}; g_assert_cmpuint(all_contacts.size(), ==, 4); seq_sort(all_contacts, [](auto&& c1, auto&& c2){return c1.email < c2.email; }); assert_equal(all_contacts[0].email, "anon@example.com"); assert_equal(all_contacts[1].email, "sqlite-dev-bounces@sqlite.org"); assert_equal(all_contacts[2].email, "sqlite-dev@sqlite.org"); assert_equal(all_contacts[3].email, "sqlite-dev@sqlite.org"); } static void test_message_attachments(void) { constexpr const char* msg_text = R"(Return-Path: <foo@example.com> Received: from pop.gmail.com [256.85.129.309] by evergrey with POP3 (fetchmail-6.4.29) for <djcb@localhost> (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET) Sender: "Foo, Example" <foo@example.com> User-agent: mu4e 1.7.11; emacs 29.0.50 From: "Foo Example" <foo@example.com> To: bar@example.com Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?= Date: Thu, 24 Mar 2022 20:04:39 +0200 Organization: ACME Inc. Message-Id: <3144HPOJ0VC77.3H1XTAG2AMTLH@"@WILSONB.COM> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" --=-=-= Content-Type: text/plain Hello, --=-=-= Content-Type: image/jpeg Content-Disposition: attachment; filename=file-01.bin Content-Transfer-Encoding: base64 AAECAw== --=-=-= Content-Type: audio/ogg Content-Disposition: inline; filename=/tmp/file-02.bin Content-Transfer-Encoding: base64 BAUGBw== --=-=-= Content-Type: message/rfc822 Content-Disposition: attachment; filename="message.eml" From: "Fnorb" <fnorb@example.com> To: Bob <bob@example.com> Subject: news for you Date: Mon, 28 Mar 2022 22:53:26 +0300 Attached message! --=-=-= Content-Type: text/plain World! --=-=-=-- )"; auto message{Message::make_from_text(msg_text)}; g_assert_true(!!message); g_assert_true(message->has_mime_message()); g_assert_true(message->path().empty()); g_assert_true(message->bcc().empty()); g_assert_true(!message->body_html()); assert_equal(message->body_text().value_or(""), R"(Hello,World!)"); g_assert_true(message->cc().empty()); g_assert_cmpuint(message->date(), ==, 1648145079); /* no Flags::Unread since it's a message without path */ g_assert_true(message->flags() == (Flags::HasAttachment)); const auto from{message->from()}; g_assert_cmpuint(from.size(),==,1); assert_equal(from.at(0).name, "Foo Example"); assert_equal(from.at(0).email, "foo@example.com"); // problem case: https://github.com/djcb/mu/issues/2232o assert_equal(message->message_id(), "3144HPOJ0VC77.3H1XTAG2AMTLH@\"@WILSONB.COM"); g_assert_true(message->path().empty()); g_assert_true(message->priority() == Priority::Normal); g_assert_cmpuint(message->size(),==,::strlen(msg_text)); /* text-based message use time({}) as their changed-time */ g_assert_cmpuint(::time({}) - message->changed(), >=, 0); g_assert_cmpuint(::time({}) - message->changed(), <=, 2); assert_equal(message->subject(), "ättächmeñts"); const auto cache_path{message->cache_path()}; g_assert_true(!!cache_path); g_assert_cmpuint(message->parts().size(),==,5); { auto&& part{message->parts().at(0)}; g_assert_false(!!part.raw_filename()); assert_equal(part.mime_type().value(), "text/plain"); assert_equal(part.to_string().value(), "Hello,"); } { auto&& part{message->parts().at(1)}; assert_equal(part.raw_filename().value(), "file-01.bin"); assert_equal(part.mime_type().value(), "image/jpeg"); // file consists of 4 bytes 0...3 g_assert_cmpuint(part.to_string()->at(0), ==, 0); g_assert_cmpuint(part.to_string()->at(1), ==, 1); g_assert_cmpuint(part.to_string()->at(2), ==, 2); g_assert_cmpuint(part.to_string()->at(3), ==, 3); } { auto&& part{message->parts().at(2)}; assert_equal(part.raw_filename().value(), "/tmp/file-02.bin"); assert_equal(part.cooked_filename().value(), "file-02.bin"); assert_equal(part.mime_type().value(), "audio/ogg"); // file consistso of 4 bytes 4..7 assert_equal(part.to_string().value(), "\004\005\006\007"); const auto fpath{*cache_path + part.cooked_filename().value()}; const auto res = part.to_file(fpath, true); g_assert_cmpuint(*res,==,4); g_assert_cmpuint(::access(fpath.c_str(), R_OK), ==, 0); } { auto&& part{message->parts().at(3)}; g_assert_true(part.mime_type() == "message/rfc822"); const auto fname{*cache_path + "/msgpart"}; g_assert_cmpuint(part.to_file(fname, true).value_or(123), ==, 139); g_assert_true(::access(fname.c_str(), F_OK) == 0); } { auto&& part{message->parts().at(4)}; g_assert_false(!!part.raw_filename()); g_assert_true(!!part.mime_type()); assert_equal(part.mime_type().value(), "text/plain"); assert_equal(part.to_string().value(), "World!"); } } /* * some test keys. */ constexpr std::string_view pub_key = R"(-----BEGIN PGP PUBLIC KEY BLOCK----- mDMEYlbaNhYJKwYBBAHaRw8BAQdAEgxZnlN3mIwqV89zchjFlEby8OgrbrkT+yRN hQhc+A+0LU11IFRlc3QgKG11IHRlc3Rpbmcga2V5KSA8bXVAZGpjYnNvZnR3YXJl Lm5sPoiUBBMWCgA8FiEE/HZRT+2bPjARz29Cw7FsU49t3vAFAmJW2jYCGwMFCwkI BwIDIgIBBhUKCQgLAgQWAgMBAh4HAheAAAoJEMOxbFOPbd7wJ2kBAIGmUDWYEPtn qYTwhZIdZtTa4KJ3UdtTqey9AnxJ9mzAAQDRJOoVppj5wW2xRhgYP+ysN2iBUYGE MhahOcNgxodbCLg4BGJW2jYSCisGAQQBl1UBBQEBB0D4Sp+GTVre7Cx5a8D3SwLJ /bRAVGDwqI7PL9B/cMmCTwMBCAeIeAQYFgoAIBYhBPx2UU/tmz4wEc9vQsOxbFOP bd7wBQJiVto2AhsMAAoJEMOxbFOPbd7w1tYA+wdfYCcwOP0QoNZZz2Yk12YkDk2R FsRrZZpb0GKC/a2VAP4qFceeSegcUCBTQaoeFE9vq9XiUVOO98QI8r9C8QwvBw== =jM/g -----END PGP PUBLIC KEY BLOCK----- )"; constexpr std::string_view priv_key = // "test1234" R"(-----BEGIN PGP PRIVATE KEY BLOCK----- lIYEYlbaNhYJKwYBBAHaRw8BAQdAEgxZnlN3mIwqV89zchjFlEby8OgrbrkT+yRN hQhc+A/+BwMCz6T2uBpk6a7/rXyE7C1bRbGjP6YSFcyRFz8VRV3Xlm7z6rdbdKZr 8R15AtLvXA4DOK5GiZRB2VbIxi8B9CtZ9qQx6YbQPkAmRzISGAjECrQtTXUgVGVz dCAobXUgdGVzdGluZyBrZXkpIDxtdUBkamNic29mdHdhcmUubmw+iJQEExYKADwW IQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbaNgIbAwULCQgHAgMiAgEGFQoJCAsC BBYCAwECHgcCF4AACgkQw7FsU49t3vAnaQEAgaZQNZgQ+2ephPCFkh1m1NrgondR 21Op7L0CfEn2bMABANEk6hWmmPnBbbFGGBg/7Kw3aIFRgYQyFqE5w2DGh1sInIsE YlbaNhIKKwYBBAGXVQEFAQEHQPhKn4ZNWt7sLHlrwPdLAsn9tEBUYPCojs8v0H9w yYJPAwEIB/4HAwI9MZDWcsoiJ/9oV5DRiAedeo3Ta/1M+aKfeNV36Ch1VGLwQF3E V77qIrJlsT8CwOZHWUksUBENvG3ak3vd84awHHaHoTmoFwtISfvQrFK0iHgEGBYK ACAWIQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbaNgIbDAAKCRDDsWxTj23e8NbW APsHX2AnMDj9EKDWWc9mJNdmJA5NkRbEa2WaW9Bigv2tlQD+KhXHnknoHFAgU0Gq HhRPb6vV4lFTjvfECPK/QvEMLwc= =w1Nc -----END PGP PRIVATE KEY BLOCK----- )"; static void test_message_signed(void) { constexpr const char *msgtext = R"(Return-Path: <diggler@gmail.com> From: Mu Test <mu@djcbsoftware.nl> To: Mu Test <mu@djcbsoftware.nl> Subject: boo Date: Wed, 13 Apr 2022 17:19:08 +0300 Message-ID: <878rs9ysin.fsf@djcbsoftware.nl> MIME-Version: 1.0 Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha512; protocol="application/pgp-signature" --=-=-= Content-Type: text/plain Sapperdeflap --=-=-= Content-Type: application/pgp-signature; name="signature.asc" -----BEGIN PGP SIGNATURE----- iIkEARYKADEWIQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbcLhMcbXVAZGpjYnNv ZnR3YXJlLm5sAAoJEMOxbFOPbd7waIkA/jK1oY7OL8vrDoubNYxamy8HHmwtvO01 Q46aYjxe0As6AP90bcAZ3dcn5RcTJaM0UhZssguawZ+tnriD3+5DPkMMCg== =e32+ -----END PGP SIGNATURE----- --=-=-=-- )"; TempDir tempdir; auto ctx{MimeCryptoContext::make_gpg(tempdir.path())}; g_assert_true(!!ctx); auto stream{MimeStream::make_mem()}; stream.write(pub_key.data(), pub_key.size()); stream.reset(); auto imported = ctx->import_keys(stream); g_assert_cmpuint(*imported, ==, 1); auto message{Message::make_from_text( msgtext, "/home/test/Maildir/inbox/cur/1649279777.107710_1.mindcrime:2,RS")}; g_assert_true(!!message); g_assert_true(message->bcc().empty()); assert_equal(message->body_text().value_or(""), "Sapperdeflap\n"); g_assert_true(message->flags() == (Flags::Signed|Flags::Seen|Flags::Replied)); size_t n{}; for (auto&& part: message->parts()) { if (!part.is_signed()) continue; const auto& mobj{part.mime_object()}; if (!mobj.is_multipart_signed()) continue; const auto mpart{MimeMultipartSigned(mobj)}; const auto sigs{mpart.verify(*ctx)}; if (!sigs) g_warning("%s", sigs.error().what()); g_assert_true(!!sigs); g_assert_cmpuint(sigs->size(), ==, 1); ++n; } g_assert_cmpuint(n, ==, 1); } static void test_message_signed_encrypted(void) { constexpr const char *msgtext = R"(From: "Mu Test" <mu@djcbsoftware.nl> To: mu@djcbsoftware.nl Subject: encrypted and signed Date: Wed, 13 Apr 2022 17:32:30 +0300 Message-ID: <87lew9xddt.fsf@djcbsoftware.nl> 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----- hF4DeEerj6WhdZASAQdAKdZwmugAlQA8c06Q5iQw4rwSADgfEWBTWlI6tDw7hEAw 0qSSeeQbA802qjG5TesaDVbFoPp1gOESt67HkJBABj9niwZLnjbzVRXKFoPTYabu 1MBWAQkCEO6kS0N73XQeJ9+nDkUacRX6sSgVM0j+nRdCGcrCQ8MOfLd9KUUBxpXy r/rIBMpZGOIpKJnoZ2x75VsQIp/ADHLe9zzXVe0tkahXJqvLo26w3gn4NSEIEDp6 4T/zMZImqGrENaixNmRiRSAnwPkLt95qJGOIqYhuW3X6hMRZyU4zDNwkAvnK+2Fv Wjd+EmiFzh5tvCmPOSj556YFMV7UpFWO9VznXX/T5+f4i+95Lsm9Uotv/SiNtNQG DPU3wiL347SzmPFXckknjlzSzDL1XbdbHdmoJs0uNnbaZxRwhkuTYbLHdpBZrBgR C0bdoCx44QVU8HaZ2x91h3GoM/0q5bqM/rvCauwbokiJgAUrznecNPY= =Ado7 -----END PGP MESSAGE----- --=-=-=-- )"; TempDir tempdir; auto ctx{MimeCryptoContext::make_gpg(tempdir.path())}; g_assert_true(!!ctx); /// test1234 // ctx->set_request_password([](const MimeCryptoContext& ctx, // const std::string& user_id, // const std::string& prompt, // bool reprompt, // MimeStream& response)->Result<void> { // return Err(Error::Code::Internal, "boo"); // //return Ok(); // }); { auto stream{MimeStream::make_mem()}; stream.write(priv_key.data(), priv_key.size()); stream.write(pub_key.data(), pub_key.size()); stream.reset(); g_assert_cmpint(ctx->import_keys(stream).value_or(-1),==,1); } auto message{Message::make_from_text( msgtext, "/home/test/Maildir/inbox/cur/1649279888.107710_1.mindcrime:2,FS")}; g_assert_true(!!message); g_assert_true(message->flags() == (Flags::Encrypted|Flags::Seen|Flags::Flagged)); size_t n{}; for (auto&& part: message->parts()) { if (!part.is_encrypted()) continue; g_assert_false(!!part.content_description()); g_assert_false(part.is_attachment()); g_assert_cmpuint(part.size(),==,0); const auto& mobj{part.mime_object()}; if (!mobj.is_multipart_encrypted()) continue; /* FIXME: make this work without user having to * type password */ // const auto mpart{MimeMultipartEncrypted(mobj)}; // const auto decres = mpart.decrypt(*ctx); // assert_valid_result(decres); ++n; } g_assert_cmpuint(n, ==, 1); } static void test_message_multipart_mixed_rfc822(void) { constexpr const char *msgtext = R"(Content-Type: multipart/mixed; boundary="Multipart_Tue_Sep__2_15:42:35_2014-1" --Multipart_Tue_Sep__2_15:42:35_2014-1 Content-Type: message/rfc822 )"; auto message{Message::make_from_text(msgtext)}; g_assert_true(!!message); //g_assert_true(message->sexp().empty()); } static void test_message_detect_attachment(void) { constexpr const char *msgtext = R"(From: "DUCK, Donald" <donald@example.com> Date: Tue, 3 May 2022 10:26:26 +0300 Message-ID: <SADKLAJCLKDJLAS-xheQjE__+hS-3tff=pTYpMUyGiJwNGF_DA@mail.gmail.com> Subject: =?Windows-1252?Q?Purkuty=F6urakka?= To: Hello <moika@example.com> Cc: =?iso-8859-1?q?M=FCller=2C?= Mickey <Mickey.Mueller@example.com> Content-Type: multipart/mixed; boundary="000000000000e687ed05de166d71" --000000000000e687ed05de166d71 Content-Type: multipart/alternative; boundary="000000000000e687eb05de166d6f" --000000000000e687eb05de166d6f Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable fyi ---------- Forwarded message --------- From: Fooish Bar <foobar@example.com> Date: Tue, 3 May 2022 at 08:59 Subject: Ty=C3=B6t To: "DUCK, Donald" <donald@example.com> Moi, -- --000000000000e687eb05de166d6f Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable abc --000000000000e687eb05de166d6f-- --000000000000e687ed05de166d71 Content-Type: application/pdf; name="test1.pdf" Content-Disposition: attachment; filename="test2.pdf" Content-Transfer-Encoding: base64 Content-ID: <18088cfd4bc5517c6321> X-Attachment-Id: 18088cfd4bc5517c6321 JVBERi0xLjcKJeLjz9MKNyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDEgMCBSIC9MYXN0 TW9kaWZpZWQgKEQ6MjAyMjA1MDMwODU3MzYrMDMnMDAnKSAvUmVzb3VyY2VzIDIgMCBSIC9NZWRp cmVmCjM1NjE4CiUlRU9GCg== --000000000000e687ed05de166d71-- )"; auto message{Message::make_from_text(msgtext)}; g_assert_true(!!message); g_assert_true(message->path().empty()); /* https://groups.google.com/g/mu-discuss/c/kCtrlxMXBjo */ g_assert_cmpuint(message->cc().size(),==, 1); assert_equal(message->cc().at(0).email, "Mickey.Mueller@example.com"); assert_equal(message->cc().at(0).name, "Müller, Mickey"); assert_equal(message->cc().at(0).display_name(), "\"Müller, Mickey\" <Mickey.Mueller@example.com>"); g_assert_true(message->bcc().empty()); assert_equal(message->subject(), "Purkutyöurakka"); assert_equal(message->body_html().value_or(""), "abc\n"); assert_equal(message->body_text().value_or(""), R"(fyi ---------- Forwarded message --------- From: Fooish Bar <foobar@example.com> Date: Tue, 3 May 2022 at 08:59 Subject: Työt To: "DUCK, Donald" <donald@example.com> Moi, -- )"); g_assert_cmpuint(message->date(), ==, 1651562786); g_assert_true(message->flags() == (Flags::HasAttachment)); g_assert_cmpuint(message->parts().size(), ==, 3); for (auto&& part: message->parts()) g_info("%s %s", part.is_attachment() ? "yes" : "no", part.mime_type().value_or("boo").c_str()); } static void test_message_calendar(void) { constexpr const char *msgtext = R"(MIME-Version: 1.0 From: William <william@example.com> To: Billy <billy@example.com> Date: Thu, 9 Jan 2014 11:09:34 +0100 Subject: Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com) Thread-Topic: Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com) Thread-Index: Ac8NIuske7OtG01VRpukb/bHE7SVHg== Message-ID: <001a11c3440066ee0b04ef86cea8@google.com> Accept-Language: en-US Content-Language: en-US X-MS-Exchange-Organization-AuthAs: Anonymous X-MS-Has-Attach: yes Content-Type: multipart/mixed; boundary="_004_001a11c3440066ee0b04ef86cea8googlecom_" --_004_001a11c3440066ee0b04ef86cea8googlecom_ Content-Type: multipart/alternative; boundary="_002_001a11c3440066ee0b04ef86cea8googlecom_" --_002_001a11c3440066ee0b04ef86cea8googlecom_ Content-Type: text/html; charset="utf-8" Content-Transfer-Encoding: base64 PGh0bWw+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0i dGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjxtZXRhIG5hbWU9IkdlbmVyYXRvciIgY29udGVu dD0iTWljcm9zb2Z0IEV4Y2hhbmdlIFNlcnZlciI+DQo8IS0tIGNvbnZlcnRlZCBmcm9tIHJ0ZiAt LT4NCjxzdHlsZT48IS0tIC5FbWFpbFF1b3RlIHsgbWFyZ2luLWxlZnQ6IDFwdDsgcGFkZGluZy1s ZWZ0OiA0cHQ7IGJvcmRlci1sZWZ0OiAjODAwMDAwIDJweCBzb2xpZDsgfSAtLT48L3N0eWxlPg0K PC9oZWFkPg0KPGJvZHk+DQo8Zm9udCBmYWNlPSJUaW1lcyBOZXcgUm9tYW4iIHNpemU9IjMiPjxh IG5hbWU9IkJNX0JFR0lOIj48L2E+DQo8dGFibGUgYm9yZGVyPSIxIiB3aWR0aD0iNzM0IiBzdHls ZT0iYm9yZGVyOjEgc29saWQ7IGJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTsgbWFyZ2luLWxlZnQ6 IDJwdDsgIj4NCjx0cj4NCjx0ZD48Zm9udCBzaXplPSIxIj48YSBocmVmPSJodHRwczovL3d3dy5n b29nbGUuY29tL2NhbGVuZGFyL2V2ZW50P2FjdGlvbj1WSUVXJmFtcDtlaWQ9YzNOemNXUXhjRGxs Ym1VeU0ySnZNbWsyYjNOeU56ZG5jRzhnWkdwallrQmthbU5pYzI5bWRIZGhjbVV1Ym13JmFtcDt0 b2s9TWpZamQybHNiR2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016 QTJaRFV3TldVMlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nh b19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT5tb3JlDQpkZXRh aWxzIMK7PC91PjwvZm9udD48L2E+PGJyPg0KDQo8ZGl2IHN0eWxlPSJtYXJnaW4tYm90dG9tOiAx NHB0OyAiPjxmb250IGZhY2U9IkFyaWFsLCBzYW5zLXNlcmlmIiBzaXplPSIyIiBjb2xvcj0iIzIy MjIyMiI+PGI+SEVMTE8sPC9iPjwvZm9udD48L2Rpdj4NCjxkaXY+PGZvbnQgc2l6ZT0iMSIgY29s b3I9IiMyMjIyMjIiPjxicj4NCg0KSSBBTSBERVNNT05EIFdJTExJQU1TIEFORCBNWSBMSVRUTEUg U0lTVEVSIElTIEdMT1JJQSwgT1VSIEZBVEhFUiBPV05TIEEgTElNSVRFRCBPRiBDT0NPQSBBTkQg R09MRCBCVVNJTkVTUyBJTiBSRVBVQkxJUVVFIERVIENPTkdPLiBBRlRFUiBISVMgVFJJUCBUTyBD T1RFIERJVk9JUkUgVE8gTkVHT1RJQVRFIE9OIENPQ09BIEFORCBHT0xEIEJVU0lORVNTIEhFIFdB TlRFRCBUTyBJTlZFU1QgSU4gQUJST0FELiA8L2ZvbnQ+PC9kaXY+DQo8ZGl2IHN0eWxlPSJtYXJn aW4tdG9wOiAxNHB0OyBtYXJnaW4tYm90dG9tOiAxNHB0OyAiPjxmb250IHNpemU9IjMiPk9ORSBX RUVLIEhFIENBTUUgQkFDSyBGUk9NIEhJUyBUUklQIFRPIEFCSURKQU4gSEUgSEFEIEEgTU9UT1Ig QUNDSURFTlQgV0lUSCBPVVIgTU9USEVSIFdISUNIIE9VUiBNT1RIRVIgRElFRCBJTlNUQU5UTFkg QlVUIE9VUiBGQVRIRVIgRElFRCBBRlRFUiBGSVZFIERBWVMgSU4gQSBQUklWQVRFIEhPU1BJVEFM IElOIE9VUiBDT1VOVFJZLg0KSVQgV0FTIExJS0UgT1VSIEZBVEhFUiBLTkVXIEhFIFdBUyBHT0lO RyBUTyBESUUgTUFZIEhJUyBHRU5UTEUgU09VTCBSRVNUIElOIFBSRUZFQ1QgUEVBQ0UuIDwvZm9u dD48L2Rpdj4NCjxkaXYgc3R5bGU9Im1hcmdpbi10b3A6IDE0cHQ7IG1hcmdpbi1ib3R0b206IDE0 cHQ7ICI+PGZvbnQgc2l6ZT0iMyI+SEUgRElTQ0xPU0VEIFRPIE1FIEFTIFRIRSBPTkxZIFNPTiBU SEFUIEhFIERFUE9TSVRFRCBUSEUgU1VNIE9GIChVU0QgJCAxMCw1MDAsMDAwKSBJTlRPIEEgQkFO SyBJTiBBQklESkFOIFRIQVQgVEhFIE1PTkVZIFdBUyBNRUFOVCBGT1IgSElTIENPQ09BIEFORCBH T0xEIEJVU0lORVNTIEhFIFdBTlRFRCBUTyBFU1RBQkxJU0ggSU4NCkFCUk9BRC5XRSBBUkUgU09M SUNJVElORyBGT1IgWU9VUiBIRUxQIFRPIFRSQU5TRkVSIFRISVMgTU9ORVkgSU5UTyBZT1VSIEFD Q09VTlQgSU4gWU9VUiBDT1VOVFJZIEZPUiBPVVIgSU5WRVNUTUVOVC4gPC9mb250PjwvZGl2Pg0K PGRpdiBzdHlsZT0ibWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9u dCBzaXplPSIzIj5QTEVBU0UgRk9SIFNFQ1VSSVRZIFJFQVNPTlMsSSBBRFZJQ0UgWU9VIFJFUExZ IFVTIFRIUk9VR0ggT1VSIFBSSVZBVEUgRU1BSUw6IDxhIGhyZWY9Im1haWx0bzp3aWxsaWFtc2Rl c21vbmQxMDdAeWFob28uY29tLnZuIj48Zm9udCBjb2xvcj0iIzAwMDBGRiI+PHU+d2lsbGlhbXNk ZXNtb25kMTA3QHlhaG9vLmNvbS52bjwvdT48L2ZvbnQ+PC9hPg0KRk9SIE1PUkUgREVUQUlMUy4g PC9mb250PjwvZGl2Pg0KPGRpdiBzdHlsZT0ibWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRv bTogMTRwdDsgIj48Zm9udCBzaXplPSIzIj5SRUdBUkRTLiA8L2ZvbnQ+PC9kaXY+DQo8ZGl2IHN0 eWxlPSJtYXJnaW4tdG9wOiAxNHB0OyBtYXJnaW4tYm90dG9tOiAxNHB0OyAiPjxmb250IHNpemU9 IjMiPkRFU01PTkQgL0dMT1JJQSBXSUxMSUFNUy48L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8dGFibGUgYm9yZGVy PSIxIiB3aWR0aD0iNzM0IiBzdHlsZT0iYm9yZGVyOjEgc29saWQ7IGJvcmRlci1jb2xsYXBzZTpj b2xsYXBzZTsgbWFyZ2luLWxlZnQ6IDJwdDsgIj4NCjxjb2wgd2lkdGg9IjM2NSI+DQo8Y29sIHdp ZHRoPSIzNjkiPg0KPHRyPg0KPHRkPjxmb250IHNpemU9IjMiPjxpPldoZW48L2k+PC9mb250Pjwv dGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJpYWwsIHNhbnMtc2VyaWYiIHNpemU9IjEiIGNvbG9yPSIj MjIyMjIyIj5UaHUgOSBKYW4gMjAxNCAwODozMCDigJMgMDk6MzAgPGZvbnQgY29sb3I9IiM4ODg4 ODgiPlNhbyBQYXVsbzwvZm9udD48L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8dHI+DQo8dGQ+PGZvbnQg c2l6ZT0iMyI+PGk+Q2FsZW5kYXI8L2k+PC9mb250PjwvdGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJp YWwsIHNhbnMtc2VyaWYiIHNpemU9IjEiIGNvbG9yPSIjMjIyMjIyIj53aWxsaWFtc19kMjBAZ2xv Ym9tYWlsLmNvbTwvZm9udD48L3RkPg0KPC90cj4NCjx0cj4NCjx0ZD48Zm9udCBzaXplPSIzIj48 aT5XaG88L2k+PC9mb250PjwvdGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJpYWwsIHNhbnMtc2VyaWYi IHNpemU9IjEiIGNvbG9yPSIjMjIyMjIyIj4oR3Vlc3QgbGlzdCBoYXMgYmVlbiBoaWRkZW4gYXQg b3JnYW5pc2VyJ3MgcmVxdWVzdCk8L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8L3RhYmxlPg0KPGRpdiBz dHlsZT0ibWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9udCBzaXplPSIxIiBjb2xvcj0iIzg4ODg4 OCI+R29pbmc/Jm5ic3A7Jm5ic3A7IDxhIGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vY2Fs ZW5kYXIvZXZlbnQ/YWN0aW9uPVJFU1BPTkQmYW1wO2VpZD1jM056Y1dReGNEbGxibVV5TTJKdk1t azJiM055TnpkbmNHOGdaR3BqWWtCa2FtTmljMjltZEhkaGNtVXVibXcmYW1wO3JzdD0xJmFtcDt0 b2s9TWpZamQybHNiR2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016 QTJaRFV3TldVMlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nh b19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT48Yj5ZZXM8L2I+ PC91PjwvZm9udD48L2E+PGZvbnQgY29sb3I9IiMyMjIyMjIiPjxiPg0KLSA8L2I+PC9mb250Pjxh IGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVJFU1BP TkQmYW1wO2VpZD1jM056Y1dReGNEbGxibVV5TTJKdk1tazJiM055TnpkbmNHOGdaR3BqWWtCa2Ft TmljMjltZEhkaGNtVXVibXcmYW1wO3JzdD0zJmFtcDt0b2s9TWpZamQybHNiR2xoYlhOZlpESXdR R2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016QTJaRFV3TldVMlltWXhOamRqTm1ZMVlU VXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nhb19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxm b250IGNvbG9yPSIjMjIwMENDIj48dT48Yj5NYXliZTwvYj48L3U+PC9mb250PjwvYT48Zm9udCBj b2xvcj0iIzIyMjIyMiI+PGI+DQotIDwvYj48L2ZvbnQ+PGEgaHJlZj0iaHR0cHM6Ly93d3cuZ29v Z2xlLmNvbS9jYWxlbmRhci9ldmVudD9hY3Rpb249UkVTUE9ORCZhbXA7ZWlkPWMzTnpjV1F4Y0Rs bGJtVXlNMkp2TW1rMmIzTnlOemRuY0c4Z1pHcGpZa0JrYW1OaWMyOW1kSGRoY21VdWJtdyZhbXA7 cnN0PTImYW1wO3Rvaz1NallqZDJsc2JHbGhiWE5mWkRJd1FHZHNiMkp2YldGcGJDNWpiMjFqTXpj MllUaGtZbUZrTXpBMlpEVXdOV1UyWW1ZeE5qZGpObVkxWVRVeE5tSmpNakU1TjJZMyZhbXA7Y3R6 PUFtZXJpY2EvU2FvX1BhdWxvJmFtcDtobD1lbl9HQiI+PGZvbnQgY29sb3I9IiMyMjAwQ0MiPjx1 PjxiPk5vPC9iPjwvdT48L2ZvbnQ+PC9hPjxmb250IGNvbG9yPSIjMjIyMjIyIj4mbmJzcDsmbmJz cDsmbmJzcDsNCjwvZm9udD48YSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFy L2V2ZW50P2FjdGlvbj1WSUVXJmFtcDtlaWQ9YzNOemNXUXhjRGxsYm1VeU0ySnZNbWsyYjNOeU56 ZG5jRzhnWkdwallrQmthbU5pYzI5bWRIZGhjbVV1Ym13JmFtcDt0b2s9TWpZamQybHNiR2xoYlhO ZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016QTJaRFV3TldVMlltWXhOamRq Tm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nhb19QYXVsbyZhbXA7aGw9ZW5f R0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT5tb3JlDQpvcHRpb25zIMK7PC91PjwvZm9udD48 L2E+PC9mb250PjwvZGl2Pg0KPC9mb250PjwvdGQ+DQo8L3RyPg0KPHRyPg0KPHRkIHN0eWxlPSJi YWNrZ3JvdW5kLWNvbG9yOiAjRjZGNkY2OyAiPjxmb250IHNpemU9IjMiPkludml0YXRpb24gZnJv bSA8YSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyLyI+PGZvbnQgY29sb3I9 IiMwMDAwRkYiPjx1Pkdvb2dsZSBDYWxlbmRhcjwvdT48L2ZvbnQ+PC9hPg0KPGRpdiBzdHlsZT0i bWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9udCBzaXplPSIzIj5Z b3UgYXJlIHJlY2VpdmluZyB0aGlzIGNvdXJ0ZXN5IGVtYWlsIGF0IHRoZSBhY2NvdW50IGRqY2JA ZGpjYnNvZnR3YXJlLm5sIGJlY2F1c2UgeW91IGFyZSBhbiBhdHRlbmRlZSBvZiB0aGlzIGV2ZW50 LjwvZm9udD48L2Rpdj4NCjxkaXYgc3R5bGU9Im1hcmdpbi10b3A6IDE0cHQ7IG1hcmdpbi1ib3R0 b206IDE0cHQ7ICI+PGZvbnQgc2l6ZT0iMyI+VG8gc3RvcCByZWNlaXZpbmcgZnV0dXJlIG5vdGlm aWNhdGlvbnMgZm9yIHRoaXMgZXZlbnQsIGRlY2xpbmUgdGhpcyBldmVudC4gQWx0ZXJuYXRpdmVs eSwgeW91IGNhbiBzaWduIHVwIGZvciBhIEdvb2dsZSBhY2NvdW50IGF0DQo8YSBocmVmPSJodHRw czovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyLyI+aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9jYWxl bmRhci88L2E+IGFuZCBjb250cm9sIHlvdXIgbm90aWZpY2F0aW9uIHNldHRpbmdzIGZvciB5b3Vy IGVudGlyZSBjYWxlbmRhci48L2ZvbnQ+PC9kaXY+DQo8L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8L3Rh YmxlPg0KPC9mb250Pg0KPC9ib2R5Pg0KPC9odG1sPg0K --_002_001a11c3440066ee0b04ef86cea8googlecom_ Content-Type: text/calendar; charset="UTF-8"; method=REQUEST Content-Transfer-Encoding: 7bit BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VEVENT DTSTART:20140109T103000Z DTEND:20140109T113000Z DTSTAMP:20140109T100934Z ORGANIZER;CN=William:mailto:william@example.com UID:sssqd1p9ene23bo2i6osr77gpo@google.com ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= TRUE;CN=billy@example.com;X-NUM-GUESTS=0:mailto:billy@example.com CREATED:20140109T100932Z DESCRIPTION:\nI AM DESMOND WILLIAMS AND MY LITTLE SISTER IS GLORIA\, OUR FA THER OWNS A LIMITED OF COCOA AND GOLD BUSINESS IN REPUBLIQUE DU CONGO. AFTE R HIS TRIP TO COTE DIVOIRE TO NEGOTIATE ON COCOA AND GOLD BUSINESS HE WANTE D TO INVEST IN ABROAD. \n\nONE WEEK HE CAME BACK FROM HIS TRIP TO ABIDJAN H E HAD A MOTOR ACCIDENT WITH OUR MOTHER WHICH OUR MOTHER DIED INSTANTLY BUT OUR FATHER DIED AFTER FIVE DAYS IN A PRIVATE HOSPITAL IN OUR COUNTRY. IT WA S LIKE OUR FATHER KNEW HE WAS GOING TO DIE MAY HIS GENTLE SOUL REST IN PREF ECT PEACE. \n\nHE DISCLOSED TO ME AS THE ONLY SON THAT HE DEPOSITED THE SUM OF (USD $ 10\,500\,000) INTO A BANK IN ABIDJAN THAT THE MONEY WAS MEANT FO R HIS COCOA AND GOLD BUSINESS HE WANTED TO ESTABLISH IN ABROAD.WE ARE SOLIC ITING FOR YOUR HELP TO TRANSFER THIS MONEY INTO YOUR ACCOUNT IN YOUR COUNTR Y FOR OUR INVESTMENT. \n\nPLEASE FOR SECURITY REASONS\,I ADVICE YOU REPLY U S THROUGH OUR PRIVATE EMAIL FOR MORE DETAI LS. \n\nREGARDS. \n\nDESMOND /GLORIA WILLIAMS.\nView your event at http://w ww.google.com/calendar/event?action=VIEW&eid=c3NzcWQxcDllbmUyM2JvMmk2b3NyNz dncG8gZGpjYkBkamNic29mdHdhcmUubmw&tok=MjYjd2lsbGlhbXNfZDIwQGdsb2JvbWFpbC5jb 21jMzc2YThkYmFkMzA2ZDUwNWU2YmYxNjdjNmY1YTUxNmJjMjE5N2Y3&ctz=America/Sao_Pau lo&hl=en_GB. LAST-MODIFIED:20140109T100932Z LOCATION: SEQUENCE:0 STATUS:CONFIRMED SUMMARY:HELLO\, TRANSP:OPAQUE END:VEVENT END:VCALENDAR --_002_001a11c3440066ee0b04ef86cea8googlecom_-- --_004_001a11c3440066ee0b04ef86cea8googlecom_ Content-Type: application/ics; name="invite.ics" Content-Description: invite.ics Content-Disposition: attachment; filename="invite.ics"; size=2029; creation-date="Thu, 09 Jan 2014 10:09:44 GMT"; modification-date="Thu, 09 Jan 2014 10:09:44 GMT" Content-Transfer-Encoding: base64 QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMTA5VDEwMzAwMFoNCkRURU5EOjIwMTQwMTA5 VDExMzAwMFoNCkRUU1RBTVA6MjAxNDAxMDlUMTAwOTM0Wg0KT1JHQU5JWkVSO0NOPVdpbGxpYW1z IFdpbGxpYW1zOm1haWx0bzp3aWxsaWFtc19kMjBAZ2xvYm9tYWlsLmNvbQ0KVUlEOnNzc3FkMXA5 ZW5lMjNibzJpNm9zcjc3Z3BvQGdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7 Q049ZGpjYkBkamNic29mdHdhcmUubmw7WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmRqY2JAZGpjYnNv ZnR3YXJlLm5sDQpDUkVBVEVEOjIwMTQwMTA5VDEwMDkzMloNCkRFU0NSSVBUSU9OOlxuSSBBTSBE RVNNT05EIFdJTExJQU1TIEFORCBNWSBMSVRUTEUgU0lTVEVSIElTIEdMT1JJQVwsIE9VUiBGQQ0K IFRIRVIgT1dOUyBBIExJTUlURUQgT0YgQ09DT0EgQU5EIEdPTEQgQlVTSU5FU1MgSU4gUkVQVUJM SVFVRSBEVSBDT05HTy4gQUZURQ0KIFIgSElTIFRSSVAgVE8gQ09URSBESVZPSVJFIFRPIE5FR09U SUFURSBPTiBDT0NPQSBBTkQgR09MRCBCVVNJTkVTUyBIRSBXQU5URQ0KIEQgVE8gSU5WRVNUIElO IEFCUk9BRC4gXG5cbk9ORSBXRUVLIEhFIENBTUUgQkFDSyBGUk9NIEhJUyBUUklQIFRPIEFCSURK QU4gSA0KIEUgSEFEIEEgTU9UT1IgQUNDSURFTlQgV0lUSCBPVVIgTU9USEVSIFdISUNIIE9VUiBN T1RIRVIgRElFRCBJTlNUQU5UTFkgQlVUIA0KIE9VUiBGQVRIRVIgRElFRCBBRlRFUiBGSVZFIERB WVMgSU4gQSBQUklWQVRFIEhPU1BJVEFMIElOIE9VUiBDT1VOVFJZLiBJVCBXQQ0KIFMgTElLRSBP VVIgRkFUSEVSIEtORVcgSEUgV0FTIEdPSU5HIFRPIERJRSBNQVkgSElTIEdFTlRMRSBTT1VMIFJF U1QgSU4gUFJFRg0KIEVDVCBQRUFDRS4gXG5cbkhFIERJU0NMT1NFRCBUTyBNRSBBUyBUSEUgT05M WSBTT04gVEhBVCBIRSBERVBPU0lURUQgVEhFIFNVTQ0KICBPRiAoVVNEICQgMTBcLDUwMFwsMDAw KSBJTlRPIEEgQkFOSyBJTiBBQklESkFOIFRIQVQgVEhFIE1PTkVZIFdBUyBNRUFOVCBGTw0KIFIg SElTIENPQ09BIEFORCBHT0xEIEJVU0lORVNTIEhFIFdBTlRFRCBUTyBFU1RBQkxJU0ggSU4gQUJS T0FELldFIEFSRSBTT0xJQw0KIElUSU5HIEZPUiBZT1VSIEhFTFAgVE8gVFJBTlNGRVIgVEhJUyBN T05FWSBJTlRPIFlPVVIgQUNDT1VOVCBJTiBZT1VSIENPVU5UUg0KIFkgRk9SIE9VUiBJTlZFU1RN RU5ULiBcblxuUExFQVNFIEZPUiBTRUNVUklUWSBSRUFTT05TXCxJIEFEVklDRSBZT1UgUkVQTFkg VQ0KIFMgVEhST1VHSCBPVVIgUFJJVkFURSBFTUFJTDogd2lsbGlhbXNkZXNtb25kMTA3QHlhaG9v LmNvbS52biBGT1IgTU9SRSBERVRBSQ0KIExTLiBcblxuUkVHQVJEUy4gXG5cbkRFU01PTkQgL0dM T1JJQSBXSUxMSUFNUy5cblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vdw0KIHd3Lmdvb2dsZS5j b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPWMzTnpjV1F4Y0RsbGJtVXlNMkp2TW1r MmIzTnlOeg0KIGRuY0c4Z1pHcGpZa0JrYW1OaWMyOW1kSGRoY21VdWJtdyZ0b2s9TWpZamQybHNi R2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYg0KIDIxak16YzJZVGhrWW1Ga016QTJaRFV3TldV MlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmY3R6PUFtZXJpY2EvU2FvX1BhdQ0KIGxvJmhs PWVuX0dCLg0KTEFTVC1NT0RJRklFRDoyMDE0MDEwOVQxMDA5MzJaDQpMT0NBVElPTjoNClNFUVVF TkNFOjANClNUQVRVUzpDT05GSVJNRUQNClNVTU1BUlk6SEVMTE9cLA0KVFJBTlNQOk9QQVFVRQ0K RU5EOlZFVkVOVA0KRU5EOlZDQUxFTkRBUg0K --_004_001a11c3440066ee0b04ef86cea8googlecom_-- )"; auto message{Message::make_from_text( msgtext, "/home/test/Maildir/inbox/cur/162342449279256.107710_1.evergrey:2,PSp")}; g_assert_true(!!message); assert_equal(message->subject(), "Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com)"); g_assert_true(message->flags() == (Flags::Passed|Flags::Seen| Flags::HasAttachment|Flags::Calendar)); g_assert_cmpuint(message->body_html().value_or("").find("DETAILS"), ==, 2271); } static void test_message_references() { constexpr auto msgtext = R"(Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 References: <YuvYh1JbE3v+abd5@kili> <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> <T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid> To: "Robin Murphy" <robin.murphy@arm.com> Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com> From: "Dan Carpenter" <dan.carpenter@oracle.com> Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs List-Id: <kernel-janitors.vger.kernel.org> Date: Fri, 5 Aug 2022 09:37:02 +0300 In-Reply-To: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> Precedence: bulk Message-Id: <20220805063702.GH3438@kadam> On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote: > On 04/08/2022 3:32 pm, Dan Carpenter wrote: > > There are two issues here: )"; auto message{Message::make_from_text( msgtext, "/home/test/Maildir/inbox/cur/162342449279256.88888_1.evergrey:2,S")}; g_assert_true(!!message); assert_equal(message->subject(), "Re: [PATCH] iommu/omap: fix buffer overflow in debugfs"); g_assert_true(message->priority() == Priority::Low); /* * "90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com" is seen both in * references and in-reply-to; in the de-duplication, the first one wins. */ std::vector<std::string> expected_refs = { "YuvYh1JbE3v+abd5@kili", "90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com", /* protonmail.internalid is fake and removed */ // "T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_" // "xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid" }; assert_equal_seq_str(expected_refs, message->references()); } static void test_message_outlook_body() { constexpr auto msgtext = R"x(Received: from vu-ex2.activedir.vu.lt (172.16.159.219) by vu-ex1.activedir.vu.lt (172.16.159.218) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1118.9 via Mailbox Transport; Fri, 27 May 2022 11:40:05 +0300 Received: from vu-ex2.activedir.vu.lt (172.16.159.219) by vu-ex2.activedir.vu.lt (172.16.159.219) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1118.9; Fri, 27 May 2022 11:40:05 +0300 Received: from vu-ex2.activedir.vu.lt ([172.16.159.219]) by vu-ex2.activedir.vu.lt ([172.16.159.219]) with mapi id 15.02.1118.009; Fri, 27 May 2022 11:40:05 +0300 From: =?windows-1257?Q?XXXXXXXXXX= <XXXXXXXXXX> To: <XXXXXXXXXX@XXXXXXXXXX.com> Subject: =?windows-1257?Q?Pra=F0ymas?= Thread-Topic: =?windows-1257?Q?Pra=F0ymas?= Thread-Index: AQHYcaRi3ejPSLxkl0uTFDto7z2OcA== Date: Fri, 27 May 2022 11:40:05 +0300 Message-ID: <5c2cd378af634e929a6cc69da1e66b9d@XX.vu.lt> Accept-Language: en-US, lt-LT Content-Language: en-US X-MS-Has-Attach: Content-Type: text/html; charset="windows-1257" Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 X-TUID: 1vFQ9RPwwg/u <html> <head> <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dwindows-1= 257"> <style type=3D"text/css" style=3D"display:none;"><!-- P {margin-top:0;margi= n-bottom:0;} --></style> </head> <body dir=3D"ltr"> <div id=3D"divtagdefaultwrapper" style=3D"font-size:12pt;color:#000000;font= -family:Calibri,Helvetica,sans-serif;" dir=3D"ltr"> <p>Laba diena visiems,</p> <p>Trumpai.</p> <p>D=EBl leidimo ar neleidimo ginti darb=E0: ed=EBstytojo paskyroje spaud= =FEiate ikon=E0 "ra=F0to darbai", atidar=E6 susiraskite =E1ra=F0= =E0 "tvirtinti / netvirtinti", pa=FEym=EBkite vien=E0 i=F0 j=F8.&= nbsp;</p> <p><br> </p> <p>=D0=E1 darb=E0 privalu atlikti, kad paskui nekilt=F8 problem=F8 studentu= i =E1vedant =E1vertinim=E0.</p> <p><br> </p> <p>Jei neleid=FEiate ginti darbo, pra=F0au informuoti mane ir komisijos sek= retori=F8.  </p> <p><br> </p> <p>Vis=E0 tolesn=E6 informacij=E0 atsi=F8siu artimiausiu metu (stengsiuosi = =F0iandien vakare).</p> <p><br> </p> <p>Pagarbiai.</p> <p><br> </p> <p><br> </p> <div id=3D"Signature"> <div id=3D"divtagdefaultwrapper" dir=3D"ltr" style=3D"font-family: Calibri,= Helvetica, sans-serif, EmojiFont, "Apple Color Emoji", "Seg= oe UI Emoji", NotoColorEmoji, "Segoe UI Symbol", "Andro= id Emoji", EmojiSymbols;"> <p style=3D"color:rgb(0,0,0); font-size:12pt"><br> </p> <p style=3D"color:rgb(0,0,0); font-size:12pt"><br> </p> <p style=3D"color:rgb(0,0,0); font-size:12pt"><br> </p> <p style=3D"color:rgb(0,0,0); font-size:12pt"><span style=3D"font-size:10pt= ; background-color:rgb(255,255,255); color:rgb(0,111,201)"><br> </span></p> <p style=3D"color:rgb(0,0,0); font-size:12pt"><span style=3D"font-size:10pt= ; background-color:rgb(255,255,255); color:rgb(0,111,201)">XXXXXXXXXX</span></p> <p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px"><= /span></font></p> <span style=3D"font-size:10pt; background-color:rgb(255,255,255); color:rgb= (0,111,201); font-size:10pt"></span> <p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p> <p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p> <p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p> <p style=3D""><br> </p> <p style=3D""><br> </p> </div> </div> </div> </body> </html> )x"; g_test_bug("2349"); auto message{Message::make_from_text( msgtext, "/home/test/Maildir/inbox/cur/162342449279256.77777_1.evergrey:2,S")}; g_assert_true(!!message); assert_equal(message->subject(), "Prašymas"); g_assert_true(message->priority() == Priority::Normal); g_assert_false(!!message->body_text()); g_assert_true(!!message->body_html()); g_assert_cmpuint(message->body_html()->find("<p>Pagarbiai.</p>"), ==, 935); } static void test_message_message_id() { constexpr const auto msg1 = R"(From: "Mu Test" <mu@djcbsoftware.nl> To: mu@djcbsoftware.nl Message-ID: <87lew9xddt.fsf@djcbsoftware.nl> abc )"; constexpr const auto msg2 = R"(From: "Mu Test" <mu@djcbsoftware.nl> To: mu@djcbsoftware.nl abc )"; constexpr const auto msg3 = R"(From: "Mu Test" <mu@djcbsoftware.nl> To: mu@djcbsoftware.nl Message-ID: abc )"; const auto m1{Message::make_from_text(msg1, "/foo/cur/m123:2,S")}; assert_valid_result(m1); const auto m2{Message::make_from_text(msg2, "/foo/cur/m456:2,S")}; assert_valid_result(m2); const auto m3{Message::make_from_text(msg3, "/foo/cur/m789:2,S")}; assert_valid_result(m3); assert_equal(m1->message_id(), "87lew9xddt.fsf@djcbsoftware.nl"); /* both with absent and empty message-id, generate "random" fake one, * which must end in @mu.id */ const auto id2{m2->message_id()}; const auto id3{m3->message_id()}; g_assert_true(g_str_has_suffix(id2.c_str(), "@mu.id")); g_assert_true(g_str_has_suffix(id3.c_str(), "@mu.id")); } static void test_message_fail () { { const auto msg = Message::make_from_path("/root/non-existent-path-12345"); g_assert_false(!!msg); } { const auto msg = Message::make_from_text("", ""); g_assert_false(!!msg); } } static void test_message_sanitize_maildir() { assert_equal(Message::sanitize_maildir("/"), "/"); assert_equal(Message::sanitize_maildir("/foo/bar"), "/foo/bar"); assert_equal(Message::sanitize_maildir("/foo/bar/cuux/"), "/foo/bar/cuux"); } static void test_message_subject_with_newline() { constexpr const auto txt = R"(To: foo@example.com Subject: =?utf-8?q?Le_poids_=C3=A9conomique_de_la_chasse_:_=0A=0Ala_dette_cach?= =?utf-8?q?=C3=A9e_de_la_chasse_!?= Date: Mon, 24 Apr 2023 07:32:43 +0000 Hello! )"; g_test_bug("2477"); const auto msg{Message::make_from_text(txt, "/foo/cur/m123:2,S")}; assert_valid_result(msg); assert_equal(msg->subject(), // newlines are filtered-out "Le poids économique de la chasse : la dette cachée de la chasse !"); assert_equal(msg->header("Subject").value_or(""), "Le poids économique de la chasse : \n\nla dette cachée de la chasse !"); } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_test_add_func("/message/message/mailing-list", test_message_mailing_list); g_test_add_func("/message/message/attachments", test_message_attachments); g_test_add_func("/message/message/signed", test_message_signed); g_test_add_func("/message/message/signed-encrypted", test_message_signed_encrypted); g_test_add_func("/message/message/multipart-mixed-rfc822", test_message_multipart_mixed_rfc822); g_test_add_func("/message/message/detect-attachment", test_message_detect_attachment); g_test_add_func("/message/message/calendar", test_message_calendar); g_test_add_func("/message/message/references", test_message_references); g_test_add_func("/message/message/outlook-body", test_message_outlook_body); g_test_add_func("/message/message/message-id", test_message_message_id); g_test_add_func("/message/message/subject-with-newline", test_message_subject_with_newline); g_test_add_func("/message/message/fail", test_message_fail); g_test_add_func("/message/message/sanitize-maildir", test_message_sanitize_maildir); return g_test_run(); } ����������������������������������������������������������������������������mu-1.10.8/lib/mu-bookmarks.cc�����������������������������������������������������������������������0000664�0000000�0000000�00000005671�14523230065�0015717�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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, 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.hh" #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 (const char*)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); } �����������������������������������������������������������������������mu-1.10.8/lib/mu-bookmarks.hh�����������������������������������������������������������������������0000664�0000000�0000000�00000004266�14523230065�0015730�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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, 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_HH__ #define MU_BOOKMARKS_HH__ #include <glib.h> /** * @addtogroup MuBookmarks * Functions for dealing with bookmarks * @{ */ /*! \struct MuBookmarks * \brief Opaque structure representing a sequence of bookmarks */ struct 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); /** @} */ #endif /*__MU_BOOKMARKS_H__*/ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-contacts-cache.cc������������������������������������������������������������������0000664�0000000�0000000�00000032440�14523230065�0016600�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2019-2022 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-cache.hh" #include <mutex> #include <unordered_map> #include <set> #include <sstream> #include <functional> #include <algorithm> #include <ctime> #include <utils/mu-utils.hh> #include <utils/mu-regex.hh> #include <glib.h> using namespace Mu; struct EmailHash { std::size_t operator()(const std::string& email) const { return lowercase_hash(email); } }; struct EmailEqual { bool operator()(const std::string& email1, const std::string& email2) const { return lowercase_hash(email1) == lowercase_hash(email2); } }; using ContactUMap = std::unordered_map<const std::string, Contact, EmailHash, EmailEqual>; struct ContactsCache::Private { Private(const std::string& serialized, const StringVec& personal) : contacts_{deserialize(serialized)}, personal_plain_{make_personal_plain(personal)}, personal_rx_{make_personal_rx(personal)}, dirty_{0} {} ContactUMap deserialize(const std::string&) const; std::string serialize() const; ContactUMap contacts_; std::mutex mtx_; const StringVec personal_plain_; const std::vector<Regex> personal_rx_; size_t dirty_; private: /** * Return the non-regex addresses * * @param personal * * @return */ StringVec make_personal_plain(const StringVec& personal) const { StringVec svec; std::copy_if(personal.begin(), personal.end(), std::back_inserter(svec), [&](auto&& p) { return p.size() < 2 || p.at(0) != '/' || p.at(p.length() - 1) != '/'; }); return svec; } /** * Return regexps for the regex-addresses * * @param personal * * @return */ std::vector<Regex> make_personal_rx(const StringVec& personal) const { std::vector<Regex> rxvec; for(auto&& p: personal) { if (p.size() < 2 || p[0] != '/' || p[p.length()- 1] != '/') continue; // a regex pattern. try { const auto rxstr{p.substr(1, p.length() - 2)}; auto opts = static_cast<GRegexCompileFlags>(G_REGEX_OPTIMIZE|G_REGEX_CASELESS); auto rx = Regex::make(rxstr, opts); if (!rx) throw rx.error(); rxvec.emplace_back(rx.value()); } catch (const Error& rex) { g_warning("invalid personal address regexp '%s': %s", p.c_str(), rex.what()); } } return rxvec; } }; constexpr auto Separator = "\xff"; // Invalid in UTF-8 ContactUMap ContactsCache::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() != 5)) { g_warning("error: '%s'", line.c_str()); continue; } Contact ci(parts[0], // email std::move(parts[1]), // name (time_t)g_ascii_strtoll(parts[3].c_str(), NULL, 10), // message_date parts[2][0] == '1' ? true : false, // personal (std::size_t)g_ascii_strtoll(parts[4].c_str(), NULL, 10), // frequency g_get_monotonic_time()); // tstamp contacts.emplace(std::move(parts[0]), std::move(ci)); } return contacts; } ContactsCache::ContactsCache(const std::string& serialized, const StringVec& personal) : priv_{std::make_unique<Private>(serialized, personal)} {} ContactsCache::~ContactsCache() = default; std::string ContactsCache::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" "%d%s" "%" G_GINT64_FORMAT "%s" "%" G_GINT64_FORMAT "\n", ci.email.c_str(), Separator, ci.name.c_str(), Separator, ci.personal ? 1 : 0, Separator, (gint64)ci.message_date, Separator, (gint64)ci.frequency); } priv_->dirty_ = 0; return s; } bool ContactsCache::dirty() const { return priv_->dirty_; } void ContactsCache::add(Contact&& contact) { /* we do _not_ cache invalid email addresses, so we won't offer them in completions etc. It * should be _rare_, but we've seen cases ( broken local messages) */ if (!contact.has_valid_email()) { g_warning("not caching invalid e-mail address '%s'", contact.email.c_str()); return; } std::lock_guard<std::mutex> l_{priv_->mtx_}; ++priv_->dirty_; auto it = priv_->contacts_.find(contact.email); if (it == priv_->contacts_.end()) { // completely new contact contact.name = contact.name; if (!contact.personal) contact.personal = is_personal(contact.email); contact.tstamp = g_get_monotonic_time(); auto email{contact.email}; // return priv_->contacts_.emplace(ContactUMap::value_type(email, std::move(contact))) // .first->second; g_debug("adding contact %s <%s>", contact.name.c_str(), contact.email.c_str()); priv_->contacts_.emplace(ContactUMap::value_type(email, std::move(contact))); } else { // existing contact. auto& existing{it->second}; ++existing.frequency; if (contact.message_date > existing.message_date) { // update? existing.email = std::move(contact.email); // update name only if new one is not empty. if (!contact.name.empty()) existing.name = std::move(contact.name); existing.tstamp = g_get_monotonic_time(); existing.message_date = contact.message_date; } g_debug("updating contact %s <%s> (%zu)", contact.name.c_str(), contact.email.c_str(), existing.frequency); } } void ContactsCache::add(Contacts&& contacts, bool& personal) { personal = seq_find_if(contacts,[&](auto&& c){ return is_personal(c.email); }) != contacts.cend(); for (auto&& contact: contacts) { contact.personal = personal; add(std::move(contact)); } } const Contact* ContactsCache::_find(const std::string& email) const { std::lock_guard<std::mutex> l_{priv_->mtx_}; const auto it = priv_->contacts_.find(email); if (it == priv_->contacts_.end()) return {}; else return &it->second; } void ContactsCache::clear() { std::lock_guard<std::mutex> l_{priv_->mtx_}; ++priv_->dirty_; priv_->contacts_.clear(); } std::size_t ContactsCache::size() const { std::lock_guard<std::mutex> l_{priv_->mtx_}; return priv_->contacts_.size(); } /** * This is used for sorting the Contacts in order of relevance. A highly * specific algorithm, but the details don't matter _too_ much. * * This is currently used for the ordering in mu-cfind and auto-completion in * mu4e, if the various completion methods don't override it... */ constexpr auto RecentOffset{15 * 24 * 3600}; struct ContactLessThan { ContactLessThan() : recently_{::time({}) - RecentOffset} {} bool operator()(const Mu::Contact& ci1, const Mu::Contact& ci2) const { // non-personal is less relevant. if (ci1.personal != ci2.personal) return ci1.personal < ci2.personal; // older is less relevant for recent messages if (std::max(ci1.message_date, ci2.message_date) > recently_ && ci1.message_date != ci2.message_date) return ci1.message_date < ci2.message_date; // less frequent is less relevant if (ci1.frequency != ci2.frequency) return ci1.frequency < ci2.frequency; // if all else fails, alphabetically return ci1.email < ci2.email; } // only sort recently seen contacts by recency; approx 15 days. // this changes during the lifetime, but that's all fine. const time_t recently_; }; using ContactSet = std::set<std::reference_wrapper<const Contact>, ContactLessThan>; void ContactsCache::for_each(const EachContactFunc& each_contact) const { std::lock_guard<std::mutex> l_{priv_->mtx_}; // first sort them for 'rank' ContactSet sorted; for (const auto& item : priv_->contacts_) sorted.emplace(item.second); // return in _reverse_ order, so we get the most relevant ones first. for (auto it = sorted.rbegin(); it != sorted.rend(); ++it) { if (!each_contact(*it)) break; } } bool ContactsCache::is_personal(const std::string& addr) const { for (auto&& p : priv_->personal_plain_) if (g_ascii_strcasecmp(addr.c_str(), p.c_str()) == 0) return true; for (auto&& rx : priv_->personal_rx_) { if (rx.matches(addr)) return true; } return false; } #ifdef BUILD_TESTS /* * Tests. * */ #include "utils/mu-test-utils.hh" static void test_mu_contacts_cache_base() { Mu::ContactsCache contacts(""); g_assert_true(contacts.empty()); g_assert_cmpuint(contacts.size(), ==, 0); contacts.add(Mu::Contact("foo.bar@example.com", "Foo", {}, 12345)); g_assert_false(contacts.empty()); g_assert_cmpuint(contacts.size(), ==, 1); contacts.add(Mu::Contact("cuux@example.com", "Cuux", {}, 54321)); g_assert_cmpuint(contacts.size(), ==, 2); contacts.add( Mu::Contact("foo.bar@example.com", "Foo", {}, 77777)); g_assert_cmpuint(contacts.size(), ==, 2); contacts.add( Mu::Contact("Foo.Bar@Example.Com", "Foo", {}, 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); } static void test_mu_contacts_cache_personal() { Mu::StringVec personal = {"foo@example.com", "bar@cuux.org", "/bar-.*@fnorb.f./"}; Mu::ContactsCache contacts{"", personal}; g_assert_true(contacts.is_personal("foo@example.com")); g_assert_true(contacts.is_personal("Bar@CuuX.orG")); g_assert_true(contacts.is_personal("bar-123abc@fnorb.fi")); g_assert_true(contacts.is_personal("bar-zzz@fnorb.fr")); g_assert_false(contacts.is_personal("foo@bar.com")); g_assert_false(contacts.is_personal("BÂr@CuuX.orG")); g_assert_false(contacts.is_personal("bar@fnorb.fi")); g_assert_false(contacts.is_personal("bar-zzz@fnorb.xr")); } static void test_mu_contacts_cache_foreach() { Mu::ContactsCache ccache(""); ccache.add(Mu::Contact{"a@example.com", "a", 123, true, 1000, 0}); ccache.add(Mu::Contact{"b@example.com", "b", 456, true, 1000, 0}); { size_t n{}; g_assert_false(ccache.empty()); g_assert_cmpuint(ccache.size(),==,2); ccache.for_each([&](auto&& contact) { ++n; return false; }); g_assert_cmpuint(n,==,1); } { size_t n{}; g_assert_false(ccache.empty()); g_assert_cmpuint(ccache.size(),==,2); ccache.for_each([&](auto&& contact) { ++n; return true; }); g_assert_cmpuint(n,==,2); } { size_t n{}; ccache.clear(); g_assert_true(ccache.empty()); g_assert_cmpuint(ccache.size(),==,0); ccache.for_each([&](auto&& contact) { ++n; return true; }); g_assert_cmpuint(n,==,0); } } static void test_mu_contacts_cache_sort() { auto result_chars = [](const Mu::ContactsCache& ccache)->std::string { std::string str; if (g_test_verbose()) g_print("contacts-cache:\n"); ccache.for_each([&](auto&& contact) { if (g_test_verbose()) g_print("\t- %s\n", contact.display_name().c_str()); str += contact.name; return true; }); return str; }; const auto now{std::time({})}; // "first" means more relevant { /* recent messages, newer comes first */ Mu::ContactsCache ccache(""); ccache.add(Mu::Contact{"a@example.com", "a", now, true, 1000, 0}); ccache.add(Mu::Contact{"b@example.com", "b", now-1, true, 1000, 0}); assert_equal(result_chars(ccache), "ab"); } { /* non-recent messages, more frequent comes first */ Mu::ContactsCache ccache(""); ccache.add(Mu::Contact{"a@example.com", "a", now-2*RecentOffset, true, 1000, 0}); ccache.add(Mu::Contact{"b@example.com", "b", now-3*RecentOffset, true, 2000, 0}); assert_equal(result_chars(ccache), "ba"); } { /* personal comes first */ Mu::ContactsCache ccache(""); ccache.add(Mu::Contact{"a@example.com", "a", now-5*RecentOffset, true, 1000, 0}); ccache.add(Mu::Contact{"b@example.com", "b", now, false, 8000, 0}); assert_equal(result_chars(ccache), "ab"); } { /* if all else fails, reverse-alphabetically */ Mu::ContactsCache ccache(""); ccache.add(Mu::Contact{"a@example.com", "a", now, false, 1000, 0}); ccache.add(Mu::Contact{"b@example.com", "b", now, false, 1000, 0}); g_assert_cmpuint(ccache.size(),==,2); assert_equal(result_chars(ccache), "ba"); } } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_test_add_func("/lib/contacts-cache/base", test_mu_contacts_cache_base); g_test_add_func("/lib/contacts-cache/personal", test_mu_contacts_cache_personal); g_test_add_func("/lib/contacts-cache/for-each", test_mu_contacts_cache_foreach); g_test_add_func("/lib/contacts-cache/sort", test_mu_contacts_cache_sort); return g_test_run(); } #endif /*BUILD_TESTS*/ ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-contacts-cache.hh������������������������������������������������������������������0000664�0000000�0000000�00000007372�14523230065�0016620�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2020-2022 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_CACHE_HH__ #define __MU_CONTACTS_CACHE_HH__ #include <glib.h> #include <time.h> #include <memory> #include <functional> #include <chrono> #include <string> #include <time.h> #include <inttypes.h> #include <utils/mu-utils.hh> #include <message/mu-message.hh> namespace Mu { class ContactsCache { public: /** * Construct a new ContactsCache object * * @param serialized serialized contacts * @param personal personal addresses */ ContactsCache(const std::string& serialized = "", const StringVec& personal = {}); /** * DTOR * */ ~ContactsCache(); /** * Add a contact * * Invalid email address are not cached (but we log a warning) * * @param contact a Contact object * */ void add(Contact&& contact); /** * Add a contacts sequence; this should be used for the contacts of a * specific message, and determines if it is a "personal" message: * if any of the contacts matches one of the personal addresses, * any of the senders/recipients are considered "personal" * * Invalid email address are not cached (but we log a warning) * * @param contacts a Contact object sequence * @param is_personal receives true if any of the contacts was personal; * false otherwise */ void add(Contacts&& contacts, bool& is_personal); void add(Contacts&& contacts) { bool _ignore; add(std::move(contacts), _ignore); } /** * 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. This all marks the data as * non-dirty (see dirty()) * * @return serialized contacts */ std::string serialize() const; /** * Has the contacts database change since the last * call to serialize()? * * @return true or false */ bool dirty() const; /** * Does this look like a 'personal' address? * * @param addr some e-mail address * * @return true or false */ bool is_personal(const std::string& addr) 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 Contact* _find(const std::string& email) const; /** * Prototype for a callable that receives a contact * * @param contact some contact * * @return to get more contacts; false otherwise */ using EachContactFunc = std::function<bool(const Contact& contact_info)>; /** * Invoke some callable for each contact, in _descending_ order of rank (i.e., the * highest ranked contacts come first). * * @param each_contact function invoked for each contact */ void for_each(const EachContactFunc& each_contact) const; private: struct Private; std::unique_ptr<Private> priv_; }; } // namespace Mu #endif /* __MU_CONTACTS_CACHE_HH__ */ ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-maildir.cc�������������������������������������������������������������������������0000664�0000000�0000000�00000030015�14523230065�0015336�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2008-2022 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. ** */ #include "config.h" #include <string> #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 <gio/gio.h> #include "glibconfig.h" #include "mu-maildir.hh" #include "utils/mu-utils.hh" #include "utils/mu-utils-file.hh" using namespace Mu; #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 */ static unsigned char get_dtype(struct dirent* dentry, const std::string& path, bool use_lstat) { #ifdef HAVE_STRUCT_DIRENT_D_TYPE if (dentry->d_type == DT_UNKNOWN) goto slowpath; if (dentry->d_type == DT_LNK && !use_lstat) goto slowpath; return dentry->d_type; /* fastpath */ slowpath: #endif /*HAVE_STRUCT_DIRENT_D_TYPE*/ return determine_dtype(path, use_lstat); } static Mu::Result<void> create_maildir(const std::string& path, mode_t mode) { if (path.empty()) return Err(Error{Error::Code::File, "path must not be empty"}); std::array<std::string,3> subdirs = {"new", "cur", "tmp"}; for (auto&& subdir: subdirs) { const auto fullpath{join_paths(path, subdir)}; /* if subdir already exists, don't try to re-create * it */ if (check_dir(fullpath, true/*readable*/, true/*writable*/)) continue; int rv{g_mkdir_with_parents(fullpath.c_str(), static_cast<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 || !check_dir(fullpath, true/*readable*/, true/*writable*/)) return Err(Error{Error::Code::File, "creating dir failed for %s: %s", fullpath.c_str(), g_strerror(errno)}); } return Ok(); } static Mu::Result<void> /* create a noindex file if requested */ create_noindex(const std::string& path) { const auto noindexpath{join_paths(path, MU_MAILDIR_NOINDEX_FILE)}; /* note, if the 'close' failed, creation may still have succeeded...*/ int fd = ::creat(noindexpath.c_str(), 0644); if (fd < 0 || ::close(fd) != 0) return Err(Error{Error::Code::File, "error creating .noindex: %s", g_strerror(errno)}); else return Ok(); } Mu::Result<void> Mu::maildir_mkdir(const std::string& path, mode_t mode, bool noindex) { if (auto&& created{create_maildir(path, mode)}; !created) return created; // fail. else if (!noindex) return Ok(); if (auto&& created{create_noindex(path)}; !created) return created; //fail return Ok(); } /* determine whether the source message is in 'new' or in 'cur'; * we ignore messages in 'tmp' for obvious reasons */ static Mu::Result<void> check_subdir(const std::string& src, bool& in_cur) { char *srcpath{g_path_get_dirname(src.c_str())}; bool invalid{}; if (g_str_has_suffix(srcpath, "cur")) in_cur = true; else if (g_str_has_suffix(srcpath, "new")) in_cur = false; else invalid = true; g_free(srcpath); if (invalid) return Err(Error{Error::Code::File, "invalid source message '%s'", src.c_str()}); else return Ok(); } static Mu::Result<std::string> get_target_fullpath(const std::string& src, const std::string& targetpath, bool unique_names) { bool in_cur{}; if (auto&& res = check_subdir(src, in_cur); !res) return Err(std::move(res.error())); const auto srcfile{to_string_gchar(g_path_get_basename(src.c_str()))}; /* 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) */ std::string fulltargetpath; if (unique_names) fulltargetpath = join_paths(targetpath, in_cur ? "cur" : "new", format("%08x-%s", g_str_hash(src.c_str()), srcfile.c_str())); else fulltargetpath = join_paths(targetpath, in_cur ? "cur" : "new", srcfile.c_str()); return fulltargetpath; } Result<void> Mu::maildir_link(const std::string& src, const std::string& targetpath, bool unique_names) { auto path_res{get_target_fullpath(src, targetpath, unique_names)}; if (!path_res) return Err(std::move(path_res.error())); auto rv{::symlink(src.c_str(), path_res->c_str())}; if (rv != 0) return Err(Error{Error::Code::File, "error creating link %s => %s: %s", path_res->c_str(), src.c_str(), g_strerror(errno)}); return Ok(); } static bool clear_links(const std::string& path, DIR* dir) { bool res; struct dirent* dentry; res = true; errno = 0; while ((dentry = ::readdir(dir))) { if (dentry->d_name[0] == '.') continue; /* ignore .,.. other dotdirs */ const auto fullpath{join_paths(path, dentry->d_name)}; const auto d_type = get_dtype(dentry, fullpath.c_str(), true/*lstat*/); switch(d_type) { case DT_LNK: if (::unlink(fullpath.c_str()) != 0) { g_warning("error unlinking %s: %s", fullpath.c_str(), g_strerror(errno)); res = false; } break; case DT_DIR: { DIR* subdir{::opendir(fullpath.c_str())}; if (!subdir) { g_warning("failed to open dir %s: %s", fullpath.c_str(), g_strerror(errno)); res = false; } if (!clear_links(fullpath, subdir)) res = false; ::closedir(subdir); } break; default: break; } } return res; } Mu::Result<void> Mu::maildir_clear_links(const std::string& path) { const auto dir{::opendir(path.c_str())}; if (!dir) return Err(Error{Error::Code::File, "failed to open %s: %s", path.c_str(), g_strerror(errno)}); clear_links(path, dir); ::closedir(dir); return Ok(); } static Mu::Result<void> msg_move_verify(const std::string& src, const std::string& dst) { /* double check -- is the target really there? */ if (::access(dst.c_str(), F_OK) != 0) return Err(Error{Error::Code::File, "can't find target (%s->%s)", src.c_str(), dst.c_str()}); if (::access(src.c_str(), F_OK) == 0) { if (src == dst) { g_warning("moved %s to itself", src.c_str()); } /* this could happen if some other tool (for mail syncing) is * interfering */ g_debug("the source is still there (%s->%s)", src.c_str(), dst.c_str()); } return Ok(); } /* use GIO to move files; this is slower than rename() so only use * this when needed: when moving across filesystems */ static Mu::Result<void> msg_move_g_file(const std::string& src, const std::string& dst) { GFile *srcfile{g_file_new_for_path(src.c_str())}; GFile *dstfile{g_file_new_for_path(dst.c_str())}; GError* err{}; auto res = g_file_move(srcfile, dstfile, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &err); g_clear_object(&srcfile); g_clear_object(&dstfile); if (res) return Ok(); else return Err(Error{Error::Code::File, &err/*consumed*/, "error moving %s -> %s", src.c_str(), dst.c_str()}); } static Mu::Result<void> msg_move(const std::string& src, const std::string& dst, bool force_gio) { if (::access(src.c_str(), R_OK) != 0) return Err(Error{Error::Code::File, "cannot read %s", src.c_str()}); if (!force_gio) { /* for testing */ if (::rename(src.c_str(), dst.c_str()) == 0) /* seems it worked; double-check */ return msg_move_verify(src, dst); if (errno != EXDEV) /* some unrecoverable error occurred */ return Err(Error{Error::Code::File, "error moving %s -> %s: %s", src.c_str(), dst.c_str(), strerror(errno)}); } /* the EXDEV / force-gio case -- source and target live on different * filesystems */ auto res = msg_move_g_file(src, dst); if (!res) return res; else return msg_move_verify(src, dst); } Mu::Result<void> Mu::maildir_move_message(const std::string& oldpath, const std::string& newpath, bool force_gio) { if (oldpath == newpath) return Ok(); // nothing to do. g_debug("moving %s --> %s", oldpath.c_str(), newpath.c_str()); return msg_move(oldpath, newpath, force_gio); } static std::string reinvent_filename_base() { return format("%u.%08x%08x.%s", static_cast<unsigned>(::time(NULL)), g_random_int(), static_cast<uint32_t>(g_get_monotonic_time()), g_get_host_name()); } /** * Determine the destination filename * * @param file a filename * @param flags flags for the destination * @param new_name whether to change the basename * * @return the destion filename. */ static std::string determine_dst_filename(const std::string& file, Flags flags, bool new_name) { /* Recalculate a unique new base file name */ auto&& parts{message_file_parts(file)}; if (new_name) parts.base = reinvent_filename_base(); /* for a New message, there are no flags etc.; so we only return the * name sans suffix */ if (any_of(flags & Flags::New)) return std::move(parts.base); const auto flagstr{ to_string( flags_filter( flags, MessageFlagCategory::Mailfile))}; return parts.base + parts.separator + "2," + flagstr; } /* * sanity checks */ static Mu::Result<void> check_determine_target_params (const std::string& old_path, const std::string& root_maildir_path, const std::string& target_maildir, Flags newflags) { if (!g_path_is_absolute(old_path.c_str())) return Err(Error{Error::Code::File, "old_path is not absolute (%s)", old_path.c_str()}); if (!g_path_is_absolute(root_maildir_path.c_str())) return Err(Error{Error::Code::File, "root maildir path is not absolute", root_maildir_path.c_str()}); if (!target_maildir.empty() && target_maildir[0] != '/') return Err(Error{Error::Code::File, "target maildir must be empty or start with / (%s)", target_maildir.c_str()}); if (old_path.find(root_maildir_path) != 0) return Err(Error{Error::Code::File, "old-path must be below root-maildir (%s) (%s)", old_path.c_str(), root_maildir_path.c_str()}); if (any_of(newflags & Flags::New) && newflags != Flags::New) return Err(Error{Error::Code::File, "if ::New is specified, " "it must be the only flag"}); return Ok(); } Mu::Result<std::string> Mu::maildir_determine_target(const std::string& old_path, const std::string& root_maildir_path, const std::string& target_maildir, Flags newflags, bool new_name) { /* sanity checks */ if (const auto checked{check_determine_target_params( old_path, root_maildir_path, target_maildir, newflags)}; !checked) return Err(Error{std::move(checked.error())}); /* * this gets us the source maildir filesystem path, the directory * in which new/ & cur/ lives, and the source file */ const auto src{base_message_dir_file(old_path)}; if (!src) return Err(src.error()); const auto& [src_mdir, src_file, is_new] = *src; /* if target_mdir is empty, the src_dir does not change (though cur/ * maybe become new or vice-versa) */ const auto dst_mdir = target_maildir.empty() ? src_mdir : join_paths(root_maildir_path, target_maildir); /* now calculate the message name (incl. its immediate parent dir) */ const auto dst_file{determine_dst_filename(src_file, newflags, new_name)}; /* and the complete path name. */ const auto subdir = std::invoke([&]()->std::string { if (none_of(newflags & Flags::New)) return "cur"; else return "new"; }); return join_paths(dst_mdir, subdir,dst_file); } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-maildir.hh�������������������������������������������������������������������������0000664�0000000�0000000�00000010367�14523230065�0015360�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2008-2022 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_HH__ #define MU_MAILDIR_HH__ #include <string> #include <utils/mu-result.hh> #include <glib.h> #include <time.h> #include <sys/types.h> /* for mode_t */ #include <message/mu-message.hh> namespace Mu { /** * 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'). * must be non-empty * @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' * * @return a valid result (!!result) or an Error */ Result<void> maildir_mkdir(const std::string& path, mode_t mode=0700, bool noindex=false); /** * 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 unique_names whether to create unique names; should be true unless * for tests. * * @return a valid result (!!result) or an Error */ Result<void> maildir_link(const std::string& src, const std::string& targetpath, bool unique_names=true); /** * Recursively delete all the symbolic links in a directory tree * * @param dir top dir * * @return a valid result (!!result) or an Error */ Result<void> maildir_clear_links(const std::string& dir); /** * Move a message file to another maildir. If the target exists, it is * overwritten. * * @param oldpath an absolute file system path to an existing message in an * actual maildir * @param newpath the absolete full path to the target file * @param force_gio force the use of GIO for moving; this is done automatically * when needed; forcing is mostly useful for tests * * @return a valid result (!!result) or an Error */ Result<void> maildir_move_message(const std::string& oldpath, const std::string& newpath, bool force_gio = false); /** * Determine the target path for a to-be-moved message; i.e. this does not * actually move the message, only calculate the path. * * @param old_path an absolute file system path to an existing message in an * actual maildir * @param root_maildir_path the absolete file system path under which * all maidlirs live. * @param target_maildir 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. Can be empty if the message should not be moved to * a different maildir; note that this may still involve a * move to another directory (say, from new/ to cur/) * @param flags to set for the target (influences the filename, path). * @param new_name whether to change the basename of the file * @param err receives error information * * @return Full path name of the target file or std::nullopt in case * of error */ Result<std::string> maildir_determine_target(const std::string& old_path, const std::string& root_maildir_path, const std::string& target_maildir, Flags newflags, bool new_name); } // namespace Mu #endif /*MU_MAILDIR_HH__*/ �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-parser.cc��������������������������������������������������������������������������0000664�0000000�0000000�00000032565�14523230065�0015225�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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 <algorithm> #include <limits> #include "mu-tokenizer.hh" #include "utils/mu-utils.hh" #include "utils/mu-error.hh" #include "utils/mu-regex.hh" #include "message/mu-message.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__)) /** * 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; Field::Id id; }; using FieldInfoVec = std::vector<FieldInfo>; struct Parser::Private { Private(const Store& store, Parser::Flags flags) : store_{store}, flags_{flags} {} std::vector<std::string> process_regex(const std::string& field, const Regex& rx) const; Mu::Tree term_1(Mu::Tokens& tokens, WarningVec& warnings) const; Mu::Tree term_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const; Mu::Tree factor_1(Mu::Tokens& tokens, WarningVec& warnings) const; Mu::Tree factor_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const; Mu::Tree unit(Mu::Tokens& tokens, WarningVec& warnings) const; Mu::Tree data(Mu::Tokens& tokens, WarningVec& warnings) const; Mu::Tree range(const FieldInfoVec& fields, const std::string& lower, const std::string& upper, size_t pos, WarningVec& warnings) const; Mu::Tree regex(const FieldInfoVec& fields, const std::string& v, size_t pos, WarningVec& warnings) const; Mu::Tree value(const FieldInfoVec& fields, const std::string& v, size_t pos, WarningVec& warnings) const; private: const Store& store_; const Parser::Flags flags_; }; static std::string process_value(const std::string& field, const std::string& value) { const auto id_opt{field_from_name(field)}; if (id_opt) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" switch (id_opt->id) { case Field::Id::Priority: { if (!value.empty()) return std::string(1, value[0]); } break; case Field::Id::Flags: if (const auto info{flag_info(value)}; info) return std::string(1, info->shortcut_lower()); break; default: break; } #pragma GCC diagnostic pop } return value; // XXX prio/flags, etc. alias } static void add_field(std::vector<FieldInfo>& fields, Field::Id field_id) { const auto field{field_from_id(field_id)}; if (!field.shortcut) return; // can't be searched fields.emplace_back(FieldInfo{std::string{field.name}, field.xapian_term(), field.is_indexable_term(), field_id}); } static std::vector<FieldInfo> process_field(const std::string& field_str, Parser::Flags flags) { std::vector<FieldInfo> fields; if (any_of(flags & Parser::Flags::UnitTest)) { add_field(fields, Field::Id::MessageId); return fields; } if (field_str == "contact" || field_str == "recip") { // multi fields add_field(fields, Field::Id::To); add_field(fields, Field::Id::Cc); add_field(fields, Field::Id::Bcc); if (field_str == "contact") add_field(fields, Field::Id::From); } else if (field_str.empty()) { add_field(fields, Field::Id::To); add_field(fields, Field::Id::Cc); add_field(fields, Field::Id::Bcc); add_field(fields, Field::Id::From); add_field(fields, Field::Id::Subject); add_field(fields, Field::Id::BodyText); } else if (const auto field_opt{field_from_name(field_str)}; field_opt) add_field(fields, field_opt->id); return fields; } static bool is_range_field(const std::string& field_str) { if (const auto field_opt{field_from_name(field_str)}; !field_opt) return false; else return field_opt->is_range(); } struct MyRange { std::string lower; std::string upper; }; static MyRange process_range(const std::string& field_str, const std::string& lower, const std::string& upper) { const auto field_opt{field_from_name(field_str)}; if (!field_opt) return {lower, upper}; std::string l2 = lower; std::string u2 = upper; constexpr auto upper_limit = std::numeric_limits<int64_t>::max(); if (field_opt->id == Field::Id::Date || field_opt->id == Field::Id::Changed) { l2 = to_lexnum(parse_date_time(lower, true).value_or(0)); u2 = to_lexnum(parse_date_time(upper, false).value_or(upper_limit)); } else if (field_opt->id == Field::Id::Size) { l2 = to_lexnum(parse_size(lower, true).value_or(0)); u2 = to_lexnum(parse_size(upper, false).value_or(upper_limit)); } return {l2, u2}; } std::vector<std::string> Parser::Private::process_regex(const std::string& field_str, const Regex& rx) const { const auto field_opt{field_from_name(field_str)}; if (!field_opt) return {}; const auto prefix{field_opt->xapian_term()}; std::vector<std::string> terms; store_.for_each_term(field_opt->id, [&](auto&& str) { auto val{str.c_str() + 1}; // strip off the Xapian prefix. if (rx.matches(val)) terms.emplace_back(std::move(val)); return true; }); return terms; } static Token look_ahead(const Mu::Tokens& tokens) { return tokens.front(); } static Mu::Tree empty() { return {{Node::Type::Empty}}; } Mu::Tree Parser::Private::value(const FieldInfoVec& fields, const std::string& v, size_t pos, WarningVec& warnings) const { 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, FieldValue{item.id, process_value(item.field, val)}}); } // a 'multi-field' such as "recip:" Tree tree(Node{Node::Type::OpOr}); for (const auto& item : fields) tree.add_child(Tree({Node::Type::Value, FieldValue{item.id, process_value(item.field, val)}})); return tree; } Mu::Tree Parser::Private::regex(const FieldInfoVec& fields, const std::string& v, size_t pos, WarningVec& warnings) const { 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 = Regex::make(rxstr, G_REGEX_OPTIMIZE); if (!rx) throw rx.error(); for (const auto& field : fields) { const auto terms = process_regex(field.field, *rx); for (const auto& term : terms) { tree.add_child(Tree({Node::Type::ValueAtomic, FieldValue{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, warnings); } } Mu::Tree Parser::Private::range(const FieldInfoVec& fields, const std::string& lower, const std::string& upper, size_t pos, WarningVec& warnings) const { if (fields.empty()) throw BUG("expected field"); const auto& field = fields.front(); if (!is_range_field(field.field)) return value(fields, lower + ".." + upper, pos, warnings); auto prange = process_range(field.field, lower, upper); if (prange.lower > prange.upper) prange = process_range(field.field, upper, lower); return Tree({Node::Type::Range, FieldValue{field.id, prange.lower, prange.upper}}); } Mu::Tree Parser::Private::data(Mu::Tokens& tokens, WarningVec& warnings) const { 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 = process_field(field, flags_); if (fields.empty()) { // not valid field... warnings.push_back({token.pos, format("invalid field '%s'", field.c_str())}); fields = process_field("", flags_); // fallback, treat the whole of foo:bar as a value return value(fields, field + ":" + val, token.pos, warnings); } // does it look like a regexp? if (val.length() >= 2) if (val[0] == '/' && val[val.length() - 1] == '/') return regex(fields, val, token.pos, 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, warnings); else if (is_range_field(fields.front().field)) { // range field without a range - treat as field:val..val return range(fields, val, val, token.pos, warnings); } // if nothing else, it's a value. return value(fields, val, token.pos, warnings); } Mu::Tree Parser::Private::unit(Mu::Tokens& tokens, WarningVec& warnings) const { 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, warnings)); return tree; } if (token.type == Token::Type::Open) { tokens.pop_front(); auto tree = term_1(tokens, 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, warnings); } Mu::Tree Parser::Private::factor_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const { if (tokens.empty()) return empty(); const auto token = look_ahead(tokens); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" 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(); } #pragma GCC diagnostic pop return factor_1(tokens, warnings); } Mu::Tree Parser::Private::factor_1(Mu::Tokens& tokens, WarningVec& warnings) const { Node::Type op{Node::Type::Invalid}; auto t = unit(tokens, warnings); auto a2 = factor_2(tokens, op, warnings); if (a2.empty()) return t; Tree tree{{op}}; tree.add_child(std::move(t)); tree.add_child(std::move(a2)); return tree; } Mu::Tree Parser::Private::term_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const { if (tokens.empty()) return empty(); const auto token = look_ahead(tokens); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" 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(); } #pragma GCC diagnostic pop tokens.pop_front(); return term_1(tokens, warnings); } Mu::Tree Parser::Private::term_1(Mu::Tokens& tokens, WarningVec& warnings) const { Node::Type op{Node::Type::Invalid}; auto t = factor_1(tokens, warnings); auto o2 = term_2(tokens, op, warnings); if (o2.empty()) return t; else { Tree tree{{op}}; tree.add_child(std::move(t)); tree.add_child(std::move(o2)); return tree; } } Mu::Parser::Parser(const Store& store, Parser::Flags flags) : priv_{std::make_unique<Private>(store, flags)} { } Mu::Parser::~Parser() = default; Mu::Tree Mu::Parser::parse(const std::string& expr, WarningVec& warnings) const { try { auto tokens = tokenize(expr); if (tokens.empty()) return empty(); else return priv_->term_1(tokens, warnings); } catch (const std::runtime_error& ex) { std::cerr << ex.what() << std::endl; return empty(); } } �������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-parser.hh��������������������������������������������������������������������������0000664�0000000�0000000�00000004555�14523230065�0015235�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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 "utils/mu-utils.hh" #include <string> #include <vector> #include <memory> #include <mu-tree.hh> #include <mu-store.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; } }; using WarningVec = std::vector<Warning>; /** * 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; } class Parser { public: enum struct Flags { None = 0, UnitTest = 1 << 0 }; /** * Construct a query parser object * * @param store a store object ptr, or none */ Parser(const Store& store, Flags = Flags::None); /** * DTOR * */ ~Parser(); /** * Parse a query string * * @param query a query string * @param warnings vec to receive warnings * * @return a parse-tree */ Tree parse(const std::string& query, WarningVec& warnings) const; private: struct Private; std::unique_ptr<Private> priv_; }; MU_ENABLE_BITOPS(Parser::Flags); } // namespace Mu #endif /* __PARSER_HH__ */ ���������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-query-match-deciders.cc������������������������������������������������������������0000664�0000000�0000000�00000016243�14523230065�0017743�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2020-2022 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-query-match-deciders.hh" #include "mu-query-results.hh" #include "utils/mu-option.hh" using namespace Mu; // We use a MatchDecider to gather information about the matches, and decide // whether to include them in the results. // // Note that to include the "related" messages, we need _two_ queries; the first // one to get the initial matches (called the Leader-Query) and a Related-Query, // to get the Leader matches + all messages that have a thread-id seen in the // Leader matches. // // We use the MatchDecider to gather information and use it for both queries. struct MatchDecider : public Xapian::MatchDecider { MatchDecider(QueryFlags qflags, DeciderInfo& info) : qflags_{qflags}, decider_info_{info} {} /** * Update the match structure with unreadable/duplicate flags * * @param doc a Xapian document. * * @return a new QueryMatch object */ QueryMatch make_query_match(const Xapian::Document& doc) const { QueryMatch qm{}; auto msgid{opt_string(doc, Field::Id::MessageId) .value_or(*opt_string(doc, Field::Id::Path))}; if (!decider_info_.message_ids.emplace(std::move(msgid)).second) qm.flags |= QueryMatch::Flags::Duplicate; const auto path{opt_string(doc, Field::Id::Path)}; if (!path || ::access(path->c_str(), R_OK) != 0) qm.flags |= QueryMatch::Flags::Unreadable; return qm; } /** * Should this message be included in the results? * * @param qm a query match * * @return true or false */ bool should_include(const QueryMatch& qm) const { if (any_of(qflags_ & QueryFlags::SkipDuplicates) && any_of(qm.flags & QueryMatch::Flags::Duplicate)) return false; if (any_of(qflags_ & QueryFlags::SkipUnreadable) && any_of(qm.flags & QueryMatch::Flags::Unreadable)) return false; return true; } /** * Gather thread ids from this match. * * @param doc the document (message) * */ void gather_thread_ids(const Xapian::Document& doc) const { auto thread_id{opt_string(doc, Field::Id::ThreadId)}; if (thread_id) decider_info_.thread_ids.emplace(std::move(*thread_id)); } protected: const QueryFlags qflags_; DeciderInfo& decider_info_; private: Option<std::string> opt_string(const Xapian::Document& doc, Field::Id id) const noexcept { const auto value_no{field_from_id(id).value_no()}; std::string val = xapian_try([&] { return doc.get_value(value_no); }, std::string{""}); if (val.empty()) return Nothing; else return Some(std::move(val)); } }; struct MatchDeciderLeader final : public MatchDecider { MatchDeciderLeader(QueryFlags qflags, DeciderInfo& info) : MatchDecider(qflags, info) {} /** * operator() * * This receives the documents considered during a Xapian query, and * is to return either true (keep) or false (ignore) * * We use this to potentiallly avoid certain messages (documents): * - with QueryFlags::SkipUnreadable this will return false for message * that are not readable in the file-system * - with QueryFlags::SkipDuplicates this will return false for messages * whose message-id was seen before. * * Even if we do not skip these messages entirely, we remember whether * they were unreadable/duplicate (in the QueryMatch::Flags), so we can * quickly find that info when doing the second 'related' query. * * The "leader" query. Matches here get the Leader flag unless they are * duplicates / unreadable. We check the duplicate/readable status * regardless of whether SkipDuplicates/SkipUnreadable was passed * (to gather that information); however those flags * affect our true/false verdict. * * @param doc xapian document * * @return true or false */ bool operator()(const Xapian::Document& doc) const override { // by definition, we haven't seen the docid before, // so no need to search auto it = decider_info_.matches.emplace(doc.get_docid(), make_query_match(doc)); it.first->second.flags |= QueryMatch::Flags::Leader; return should_include(it.first->second); } }; std::unique_ptr<Xapian::MatchDecider> Mu::make_leader_decider(QueryFlags qflags, DeciderInfo& info) { return std::make_unique<MatchDeciderLeader>(qflags, info); } struct MatchDeciderRelated final : public MatchDecider { MatchDeciderRelated(QueryFlags qflags, DeciderInfo& info) : MatchDecider(qflags, info) {} /** * operator() * * This receives the documents considered during a Xapian query, and * is to return either true (keep) or false (ignore) * * We use this to potentially avoid certain messages (documents): * - with QueryFlags::SkipUnreadable this will return false for message * that are not readable in the file-system * - with QueryFlags::SkipDuplicates this will return false for messages * whose message-id was seen before. * * Unlike in the "leader" decider (scroll up), we don't need to remember * messages we won't include. * * @param doc xapian document * * @return true or false */ bool operator()(const Xapian::Document& doc) const override { // we may have seen this match in the "Leader" query. const auto it = decider_info_.matches.find(doc.get_docid()); if (it != decider_info_.matches.end()) return should_include(it->second); auto qm{make_query_match(doc)}; if (should_include(qm)) { qm.flags |= QueryMatch::Flags::Related; decider_info_.matches.emplace(doc.get_docid(), std::move(qm)); return true; } else return false; // nope. } }; std::unique_ptr<Xapian::MatchDecider> Mu::make_related_decider(QueryFlags qflags, DeciderInfo& info) { return std::make_unique<MatchDeciderRelated>(qflags, info); } struct MatchDeciderThread final : public MatchDecider { MatchDeciderThread(QueryFlags qflags, DeciderInfo& info) : MatchDecider{qflags, info} {} /** * operator() * * This receives the documents considered during a Xapian query, and * is to return either true (keep) or false (ignore) * * Only include documents that earlier checks have decided to include. * * @param doc xapian document * * @return true or false */ bool operator()(const Xapian::Document& doc) const override { // we may have seen this match in the "Leader" query, // or in the second (unbuounded) related query; const auto it{decider_info_.matches.find(doc.get_docid())}; return it != decider_info_.matches.end() && !it->second.thread_path.empty(); } }; std::unique_ptr<Xapian::MatchDecider> Mu::make_thread_decider(QueryFlags qflags, DeciderInfo& info) { return std::make_unique<MatchDeciderThread>(qflags, info); } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-query-match-deciders.hh������������������������������������������������������������0000664�0000000�0000000�00000004577�14523230065�0017764�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2021 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_QUERY_MATCH_DECIDERS_HH__ #define MU_QUERY_MATCH_DECIDERS_HH__ #include <unordered_set> #include <unordered_map> #include <memory> #include <xapian.h> #include "mu-query-results.hh" namespace Mu { using StringSet = std::unordered_set<std::string>; struct DeciderInfo { QueryMatches matches; StringSet thread_ids; StringSet message_ids; }; /** * Make a "leader" decider, that is, a MatchDecider for either a singular or the * first query in the leader/related pair of queries. Gather information for * threading, and the subsequent "related" query. * * @param qflags query flags * @param match_info receives information about the matches. * * @return a unique_ptr to a match decider. */ std::unique_ptr<Xapian::MatchDecider> make_leader_decider(QueryFlags qflags, DeciderInfo& info); /** * Make a "related" decider, that is, a MatchDecider for the second query * in the leader/related pair of queries. * * @param qflags query flags * @param match_info receives information about the matches. * * @return a unique_ptr to a match decider. */ std::unique_ptr<Xapian::MatchDecider> make_related_decider(QueryFlags qflags, DeciderInfo& info); /** * Make a "thread" decider, that is, a MatchDecider that removes all but the * document excepts for the ones found during initial/related searches. * * @param qflags query flags * @param match_info receives information about the matches. * * @return a unique_ptr to a match decider. */ std::unique_ptr<Xapian::MatchDecider> make_thread_decider(QueryFlags qflags, DeciderInfo& info); } // namespace Mu #endif /* MU_QUERY_MATCH_DECIDERS_HH__ */ ���������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-query-results.hh�������������������������������������������������������������������0000664�0000000�0000000�00000026201�14523230065�0016575�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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_QUERY_RESULTS_HH__ #define MU_QUERY_RESULTS_HH__ #include <algorithm> #include <limits> #include <stdexcept> #include <string> #include <unordered_map> #include <unordered_set> #include <limits> #include <ostream> #include <cmath> #include <memory> #include <unistd.h> #include <fcntl.h> #include <xapian.h> #include <glib.h> #include <utils/mu-utils.hh> #include <utils/mu-option.hh> #include <utils/mu-xapian-utils.hh> #include <message/mu-message.hh> namespace Mu { /** * This implements a QueryResults structure, which capture the results of a * Xapian query, and a QueryResultsIterator, which gives C++-compliant iterator * to go over the results. and finally QueryThreader (in query-threader.cc) which * calculates the threads, using the JWZ algorithm. */ /// Flags that influence now matches are presented (or skipped) enum struct QueryFlags { None = 0, /**< no flags */ Descending = 1 << 0, /**< sort z->a */ SkipUnreadable = 1 << 1, /**< skip unreadable msgs */ SkipDuplicates = 1 << 2, /**< skip duplicate msgs */ IncludeRelated = 1 << 3, /**< include related msgs */ Threading = 1 << 4, /**< calculate threading info */ // internal Leader = 1 << 5, /**< This is the leader query (for internal use * only)*/ }; MU_ENABLE_BITOPS(QueryFlags); /// Stores all the essential information for sorting the results. struct QueryMatch { /// Flags for a match (message) found enum struct Flags { None = 0, /**< No Flags */ Leader = 1 << 0, /**< Mark direct matches as leader */ Related = 1 << 1, /**< A related message */ Unreadable = 1 << 2, /**< No readable file */ Duplicate = 1 << 3, /**< Message-id seen before */ Root = 1 << 10, /**< Is this the thread-root? */ First = 1 << 11, /**< Is this the first message in a thread? */ Last = 1 << 12, /**< Is this the last message in a thread? */ Orphan = 1 << 13, /**< Is this message without a parent? */ HasChild = 1 << 14, /**< Does this message have a child? */ ThreadSubject = 1 << 20, /**< Message holds subject for (sub)thread */ }; Flags flags{Flags::None}; /**< Flags */ std::string date_key; /**< The date-key (for sorting all sub-root levels) */ // the thread subject is the subject of the first message in a thread, // and any message that has a different subject compared to its predecessor // (ignoring prefixes such as Re:) // // otherwise, it is empty. std::string subject; /**< subject for this message */ size_t thread_level{}; /**< The thread level */ std::string thread_path; /**< The hex-numerial path in the thread, ie. '00:01:0a' */ std::string thread_date; /**< date of newest message in thread */ bool operator<(const QueryMatch& rhs) const { return date_key < rhs.date_key; } bool has_flag(Flags flag) const; }; MU_ENABLE_BITOPS(QueryMatch::Flags); inline bool QueryMatch::has_flag(QueryMatch::Flags flag) const { return any_of(flags & flag); } inline std::ostream& operator<<(std::ostream& os, QueryMatch::Flags mflags) { if (mflags == QueryMatch::Flags::None) { os << "<none>"; return os; } if (any_of(mflags & QueryMatch::Flags::Leader)) os << "leader "; if (any_of(mflags & QueryMatch::Flags::Unreadable)) os << "unreadable "; if (any_of(mflags & QueryMatch::Flags::Duplicate)) os << "dup "; if (any_of(mflags & QueryMatch::Flags::Root)) os << "root "; if (any_of(mflags & QueryMatch::Flags::Related)) os << "related "; if (any_of(mflags & QueryMatch::Flags::First)) os << "first "; if (any_of(mflags & QueryMatch::Flags::Last)) os << "last "; if (any_of(mflags & QueryMatch::Flags::Orphan)) os << "orphan "; if (any_of(mflags & QueryMatch::Flags::HasChild)) os << "has-child "; return os; } using QueryMatches = std::unordered_map<Xapian::docid, QueryMatch>; inline std::ostream& operator<<(std::ostream& os, const QueryMatch& qmatch) { os << "qm:[" << qmatch.thread_path << "]: " // " (" << qmatch.thread_level << "): " << "> date:<" << qmatch.date_key << "> " << "flags:{" << qmatch.flags << "}"; return os; } /// /// This is a view over the Xapian::MSet, which can optionally filter unreadable /// / duplicate messages. /// /// Note, we internally skip unreadable/duplicate messages (when asked too); those /// skipped ones do _not_ count towards the max_size /// class QueryResultsIterator { public: using iterator_category = std::output_iterator_tag; using value_type = Message; using difference_type = void; using pointer = void; using reference = void; QueryResultsIterator(Xapian::MSetIterator mset_it, QueryMatches& query_matches) : mset_it_{mset_it}, query_matches_{query_matches} { } /** * Increment the iterator (we don't support post-increment) * * @return an updated iterator, or end() if we were already at end() */ QueryResultsIterator& operator++() { ++mset_it_; mdoc_ = Nothing; return *this; } /** * (Non)Equivalence operators * * @param rhs some other iterator * * @return true or false */ bool operator==(const QueryResultsIterator& rhs) const { return mset_it_ == rhs.mset_it_; } bool operator!=(const QueryResultsIterator& rhs) const { return mset_it_ != rhs.mset_it_; } QueryResultsIterator& operator*() { return *this; } const QueryResultsIterator& operator*() const { return *this; } /** * Get the Xapian::Document this iterator is pointing at, * or an empty document when looking at end(). * * @return a document */ Option<Xapian::Document> document() const { return xapian_try([this]()->Option<Xapian::Document> { auto doc{mset_it_.get_document()}; if (doc.get_docid() == 0) return Nothing; else return Some(std::move(doc)); }, Nothing); } /** * get the corresponding Message for this iter, if any * * @return a Message or Nothing */ Option<Message> message() const { if (auto&& xdoc{document()}; !xdoc) return Nothing; else if (auto&& doc{Message::make_from_document(std::move(xdoc.value()))}; !doc) return Nothing; else return Some(std::move(doc.value())); } /** * Get the doc-id for the document this iterator is pointing at, or 0 * when looking at end. * * @return a doc-id. */ Xapian::docid doc_id() const { return *mset_it_; } /** * Get the message-id for the document (message) this iterator is * pointing at, or not when not available * * @return a message-id */ Option<std::string> message_id() const noexcept { return opt_string(Field::Id::MessageId); } /** * Get the thread-id for the document (message) this iterator is * pointing at, or Nothing. * * @return a message-id */ Option<std::string> thread_id() const noexcept { return opt_string(Field::Id::ThreadId); } /** * Get the file-system path for the document (message) this iterator is * pointing at, or Nothing. * * @return a filesystem path */ Option<std::string> path() const noexcept { return opt_string(Field::Id::Path); } /** * Get the a sortable date str for the document (message) the iterator * is pointing at. pointing at, or Nothing. This (encoded) string * has the same sort-order as the corresponding date. * * @return a filesystem path */ Option<std::string> date_str() const noexcept { return opt_string(Field::Id::Date); } /** * Get the subject for the document (message) this iterator is pointing * at. * * @return the subject */ Option<std::string> subject() const noexcept { return opt_string(Field::Id::Subject); } /** * Get the references for the document (messages) this is iterator is * pointing at, or empty if pointing at end of if no references are * available. * * @return references */ std::vector<std::string> references() const noexcept { return mu_document().string_vec_value(Field::Id::References); } /** * Get some value from the document, or Nothing if empty. * * @param id a message field id * * @return the value */ Option<std::string> opt_string(Field::Id id) const noexcept { if (auto&& val{mu_document().string_value(id)}; val.empty()) return Nothing; else return Some(std::move(val)); } /** * Get the Query match info for this message. * * @return the match info. */ QueryMatch& query_match() { g_assert(query_matches_.find(doc_id()) != query_matches_.end()); return query_matches_.find(doc_id())->second; } const QueryMatch& query_match() const { g_assert(query_matches_.find(doc_id()) != query_matches_.end()); return query_matches_.find(doc_id())->second; } private: /** * Get a (cached) reference for the Mu::Document corresponding * to the current iter. * * @return cached mu document, */ const Mu::Document& mu_document() const { if (!mdoc_) { if (auto xdoc = document(); !xdoc) std::runtime_error("iter without document"); else mdoc_ = Mu::Document{xdoc.value()}; } return mdoc_.value(); } mutable Option<Mu::Document> mdoc_; // cache. Xapian::MSetIterator mset_it_; QueryMatches& query_matches_; }; constexpr auto MaxQueryResultsSize = std::numeric_limits<size_t>::max(); class QueryResults { public: /// Helper types using iterator = QueryResultsIterator; using const_iterator = const iterator; /** * Construct a QueryResults object * * @param mset an Xapian::MSet with matches */ QueryResults(const Xapian::MSet& mset, QueryMatches&& query_matches) : mset_{mset}, query_matches_{std::move(query_matches)} { } /** * Is this QueryResults object empty (ie., no matches)? * * @return true are false */ bool empty() const { return mset_.empty(); } /** * Get the number of matches in this QueryResult * * @return number of matches */ size_t size() const { return mset_.size(); } /** * Get the begin iterator to the results. * * @return iterator */ const iterator begin() const { return QueryResultsIterator(mset_.begin(), query_matches_); } /** * Get the end iterator to the results. * * @return iterator */ const_iterator end() const { return QueryResultsIterator(mset_.end(), query_matches_); } /** * Get the query-matches for these QueryResults. The non-const * version can be use to _steal_ the query results, by moving * them. * * @return query-matches */ const QueryMatches& query_matches() const { return query_matches_; } QueryMatches& query_matches() { return query_matches_; } private: const Xapian::MSet mset_; mutable QueryMatches query_matches_; }; } // namespace Mu #endif /* MU_QUERY_RESULTS_HH__ */ �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-query-threads.cc�������������������������������������������������������������������0000664�0000000�0000000�00000065130�14523230065�0016520�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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-query-threads.hh" #include <message/mu-message.hh> #include <set> #include <unordered_set> #include <list> #include <cassert> #include <cstring> #include <iostream> #include <iomanip> #include <utils/mu-option.hh> using namespace Mu; struct Container { using Containers = std::vector<Container*>; Container() = default; Container(Option<QueryMatch&> msg) : query_match{msg} {} Container(const Container&) = delete; Container(Container&&) = default; void add_child(Container& new_child) { new_child.parent = this; children.emplace_back(&new_child); } void remove_child(Container& child) { children.erase(find_child(child)); assert(!has_child(child)); } Containers::iterator find_child(Container& child) { return std::find_if(children.begin(), children.end(), [&](auto&& c) { return c == &child; }); } Containers::const_iterator find_child(Container& child) const { return std::find_if(children.begin(), children.end(), [&](auto&& c) { return c == &child; }); } bool has_child(Container& child) const { return find_child(child) != children.cend(); } bool is_reachable(Container* other) const { auto up{ur_parent()}; return up && up == other->ur_parent(); } template <typename Func> void for_each_child(Func&& func) { auto it{children.rbegin()}; while (it != children.rend()) { auto next = std::next(it); func(*it); it = next; } } // During sorting, this is the cached value for the (recursive) date-key // of this container -- ie.. either the one from the first of its // children, or from its query-match, if it has no children. // // Note that the sub-root-levels of threads are always sorted by date, // in ascending order, regardless of whatever sorting was specified for // the root-level. std::string thread_date_key; Option<QueryMatch&> query_match; bool is_nuked{}; Container* parent{}; Containers children; using ContainerVec = std::vector<Container*>; private: const Container* ur_parent() const { assert(this->parent != this); return parent ? parent->ur_parent() : this; } }; using Containers = Container::Containers; using ContainerVec = Container::ContainerVec; static std::ostream& operator<<(std::ostream& os, const Container& container) { os << "container: " << std::right << std::setw(10) << &container << ": parent: " << std::right << std::setw(10) << container.parent << " [" << container.thread_date_key << "]" << "\n children: "; for (auto&& c : container.children) os << std::right << std::setw(10) << c << " "; os << (container.is_nuked ? " nuked" : ""); if (container.query_match) os << "\n " << container.query_match.value(); return os; } using IdTable = std::unordered_map<std::string, Container>; using DupTable = std::multimap<std::string, Container>; static void handle_duplicates(IdTable& id_table, DupTable& dup_table) { size_t n{}; for (auto&& dup : dup_table) { const auto msgid{dup.first}; auto it = id_table.find(msgid); if (it == id_table.end()) continue; // add duplicates as fake children char buf[32]; ::snprintf(buf, sizeof(buf), "dup-%zu", ++n); it->second.add_child(id_table.emplace(buf, std::move(dup.second)).first->second); } } template <typename QueryResultsType> static IdTable determine_id_table(QueryResultsType& qres) { // 1. For each query_match IdTable id_table; DupTable dups; for (auto&& mi : qres) { const auto msgid{mi.message_id().value_or(*mi.path())}; // Step 0 (non-JWZ): filter out dups, handle those at the end if (mi.query_match().has_flag(QueryMatch::Flags::Duplicate)) { dups.emplace(msgid, mi.query_match()); continue; } // 1.A If id_table contains an empty Container for this ID: // Store this query_match (query_match) in the Container's query_match (value) slot. // Else: // Create a new Container object holding this query_match (query-match); // Index the Container by Query_Match-ID auto c_it = id_table.find(msgid); auto& container = [&]() -> Container& { if (c_it != id_table.end()) { if (!c_it->second.query_match) // hmm, dup? c_it->second.query_match = mi.query_match(); return c_it->second; } else { // Else: // Create a new Container object holding this query_match // (query-match); Index the Container by Query_Match-ID return id_table.emplace(msgid, mi.query_match()).first->second; } }(); // We sort by date (ascending), *except* for the root; we don't // know what query_matchs will be at the root level yet, so remember // both. Moreover, even when sorting the top-level in descending // order, still sort the thread levels below that in ascending // order. container.thread_date_key = container.query_match->date_key = mi.date_str().value_or(""); // initial guess for the thread-date; might be updated // later. // remember the subject, we use it to determine the (sub)thread subject container.query_match->subject = mi.subject().value_or(""); // 1.B // For each element in the query_match's References field: Container* parent_ref_container{}; for (const auto& ref : mi.references()) { // grand_<n>-parent -> grand_<n-1>-parent -> ... -> parent. // Find a Container object for the given Query_Match-ID; If it exists, use // it; otherwise make one with a null Query_Match. auto ref_container = [&]() -> Container* { auto ref_it = id_table.find(ref); if (ref_it == id_table.end()) ref_it = id_table.emplace(ref, Nothing).first; return &ref_it->second; }(); // Link the References field's Containers 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 (parent_ref_container && !ref_container->parent) { if (!parent_ref_container->is_reachable(ref_container)) parent_ref_container->add_child(*ref_container); // else // g_message ("%u: reachable %s -> %s", __LINE__, // msgid.c_str(), ref.c_str()); } parent_ref_container = ref_container; } // Add the query_match to the chain. if (parent_ref_container && !container.parent) { if (!parent_ref_container->is_reachable(&container)) parent_ref_container->add_child(container); // else // g_message ("%u: reachable %s -> parent", __LINE__, // msgid.c_str()); } } // non-JWZ: add duplicate messages. handle_duplicates(id_table, dups); return id_table; } /// Recursively walk all containers under the root set. /// For each container: /// /// If it is an empty container with no children, nuke it. /// /// Note: Normally such containers won't occur, but they can show up when two /// query_matchs have References lines that disagree. For example, assuming A and /// B are query_matchs, and 1, 2, and 3 are references for query_matchs we haven't /// seen: /// /// A has references: 1, 2, 3 /// B has references: 1, 3 /// /// There is ambiguity as to whether 3 is a child of 1 or of 2. So, /// depending on the processing order, we might end up with either /// /// -- 1 /// |-- 2 /// \-- 3 /// |-- A /// \-- B /// /// or /// /// -- 1 /// |-- 2 <--- non root childless container! /// \-- 3 /// |-- A /// \-- B /// /// If the Container has no Query_Match, 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. static void prune(Container* child) { Container* container{child->parent}; for (auto& grandchild : child->children) { grandchild->parent = container; if (container) container->children.emplace_back(grandchild); } child->children.clear(); child->is_nuked = true; if (container) container->remove_child(*child); } static bool prune_empty_containers(Container& container) { Containers to_prune; container.for_each_child([&](auto& child) { if (prune_empty_containers(*child)) to_prune.emplace_back(child); }); for (auto& child : to_prune) prune(child); // Never nuke these. if (container.query_match) return false; // If it is an empty container with no children, nuke it. // // If the Container is empty, 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. // const auto rootset_child{!container.parent->parent}; if (container.parent || container.children.size() <= 1) return true; // splice/nuke it. return false; } static void prune_empty_containers(IdTable& id_table) { for (auto&& item : id_table) { auto& child(item.second); if (child.parent) continue; // not a root child. if (prune_empty_containers(item.second)) prune(&child); } } // // Sorting. // /// Register some information about a match (i.e., message) that we can use for /// subsequent queries. using ThreadPath = std::vector<unsigned>; inline std::string to_string(const ThreadPath& tpath, size_t digits) { std::string str; str.reserve(tpath.size() * digits); bool first{true}; for (auto&& segm : tpath) { str += format("%s%0*x", first ? "" : ":", (int)digits, segm); first = false; } return str; } static bool // compare subjects, ignore anything before the last ':<space>*' subject_matches(const std::string& sub1, const std::string& sub2) { auto search_str = [](const std::string& s) -> const char* { const auto pos = s.find_last_of(':'); if (pos == std::string::npos) return s.c_str(); else { const auto pos2 = s.find_first_not_of(' ', pos + 1); return s.c_str() + (pos2 == std::string::npos ? pos : pos2); } }; // g_debug ("'%s' '%s'", search_str(sub1), search_str(sub2)); return g_strcmp0(search_str(sub1), search_str(sub2)) == 0; } static bool update_container(Container& container, bool descending, ThreadPath& tpath, size_t seg_size, const std::string& prev_subject = "") { if (!container.children.empty()) { Container* first = container.children.front(); if (first->query_match) first->query_match->flags |= QueryMatch::Flags::First; Container* last = container.children.back(); if (last->query_match) last->query_match->flags |= QueryMatch::Flags::Last; } if (!container.query_match) return false; // nothing else to do. auto& qmatch(*container.query_match); if (!container.parent) qmatch.flags |= QueryMatch::Flags::Root; else if (!container.parent->query_match) qmatch.flags |= QueryMatch::Flags::Orphan; if (!container.children.empty()) qmatch.flags |= QueryMatch::Flags::HasChild; if (qmatch.has_flag(QueryMatch::Flags::Root) || prev_subject.empty() || !subject_matches(prev_subject, qmatch.subject)) qmatch.flags |= QueryMatch::Flags::ThreadSubject; if (descending && container.parent) { // trick xapian by giving it "inverse" sorting key so our // ascending-date sorted threads stay in that order tpath.back() = ((1U << (4 * seg_size)) - 1) - tpath.back(); } qmatch.thread_path = to_string(tpath, seg_size); qmatch.thread_level = tpath.size() - 1; // ensure thread root comes before its children if (descending) qmatch.thread_path += ":z"; return true; } static void update_containers(Containers& children, bool descending, ThreadPath& tpath, size_t seg_size, std::string& prev_subject) { size_t idx{0}; for (auto&& c : children) { tpath.emplace_back(idx++); if (c->query_match) { update_container(*c, descending, tpath, seg_size, prev_subject); prev_subject = c->query_match->subject; } update_containers(c->children, descending, tpath, seg_size, prev_subject); tpath.pop_back(); } } static void update_containers(ContainerVec& root_vec, bool descending, size_t n) { ThreadPath tpath; tpath.reserve(n); const auto seg_size = static_cast<size_t>(std::ceil(std::log2(n) / 4.0)); /*note: 4 == std::log2(16)*/ size_t idx{0}; for (auto&& c : root_vec) { tpath.emplace_back(idx++); std::string prev_subject; if (update_container(*c, descending, tpath, seg_size)) prev_subject = c->query_match->subject; update_containers(c->children, descending, tpath, seg_size, prev_subject); tpath.pop_back(); } } static void sort_container(Container& container) { // 1. childless container. if (container.children.empty()) return; // no children; nothing to sort. // 2. container with children. // recurse, depth-first: sort the children for (auto& child : container.children) sort_container(*child); // now sort this level. std::sort(container.children.begin(), container.children.end(), [&](auto&& c1, auto&& c2) { return c1->thread_date_key < c2->thread_date_key; }); // and 'bubble up' the date of the *newest* message with a date. We // reasonably assume that it's later than its parent. const auto& newest_date = container.children.back()->thread_date_key; if (!newest_date.empty()) container.thread_date_key = newest_date; } static void sort_siblings(IdTable& id_table, bool descending) { if (id_table.empty()) return; // unsorted vec of root containers. We can // only sort these _after_ sorting the children. ContainerVec root_vec; for (auto&& item : id_table) { if (!item.second.parent && !item.second.is_nuked) root_vec.emplace_back(&item.second); } // now sort all threads _under_ the root set (by date/ascending) for (auto&& c : root_vec) sort_container(*c); // and then sort the root set. // // The difference with the sub-root containers is that at the top-level, // we can sort either in ascending or descending order, while on the // subroot level it's always in ascending order. // // Note that unless we're testing, _xapian_ will handle // the ascending/descending of the top level. std::sort(root_vec.begin(), root_vec.end(), [&](auto&& c1, auto&& c2) { #ifdef BUILD_TESTS if (descending) return c2->thread_date_key < c1->thread_date_key; else #endif /*BUILD_TESTS*/ return c1->thread_date_key < c2->thread_date_key; }); // now all is sorted... final step is to determine thread paths and // other flags. update_containers(root_vec, descending, id_table.size()); } static std::ostream& operator<<(std::ostream& os, const IdTable& id_table) { os << "------------------------------------------------\n"; for (auto&& item : id_table) { os << item.first << " => " << item.second << "\n"; } os << "------------------------------------------------\n"; std::set<std::string> ids; for (auto&& item : id_table) { if (item.second.query_match) ids.emplace(item.second.query_match->thread_path); } for (auto&& id : ids) { auto it = std::find_if(id_table.begin(), id_table.end(), [&](auto&& item) { return item.second.query_match && item.second.query_match->thread_path == id; }); assert(it != id_table.end()); os << it->first << ": " << it->second << '\n'; } return os; } template <typename Results> static void calculate_threads_real(Results& qres, bool descending) { // Step 1: build the id_table auto id_table{determine_id_table(qres)}; if (g_test_verbose()) std::cout << "*** id-table(1):\n" << id_table << "\n"; // // Step 2: get the root set // // Step 3: discard id_table // Nope: id-table owns the containers. // Step 4: prune empty containers prune_empty_containers(id_table); // Step 5: group root-set by subject. // Not implemented. // Step 6: we're done threading // Step 7: sort siblings. The segment-size is the number of hex-digits // in the thread-path string (so we can lexically compare them.) sort_siblings(id_table, descending); // Step 7a:. update querymatches for (auto&& item : id_table) { Container& c{item.second}; if (c.query_match) c.query_match->thread_date = c.thread_date_key; } // if (g_test_verbose()) // std::cout << "*** id-table(2):\n" << id_table << "\n"; } void Mu::calculate_threads(Mu::QueryResults& qres, bool descending) { calculate_threads_real(qres, descending); } #ifdef BUILD_TESTS struct MockQueryResult { MockQueryResult(const std::string& message_id_arg, const std::string& date_arg, const std::vector<std::string>& refs_arg = {}) : message_id_{message_id_arg}, date_{date_arg}, refs_{refs_arg} { } MockQueryResult(const std::string& message_id_arg, const std::vector<std::string>& refs_arg = {}) : MockQueryResult(message_id_arg, "", refs_arg) { } Option<std::string> message_id() const { return message_id_; } Option<std::string> path() const { return path_; } Option<std::string> date_str() const { return date_; } Option<std::string> subject() const { return subject_; } QueryMatch& query_match() { return query_match_; } const QueryMatch& query_match() const { return query_match_; } const std::vector<std::string>& references() const { return refs_; } std::string path_; std::string message_id_; QueryMatch query_match_{}; std::string date_; std::string subject_; std::vector<std::string> refs_; }; using MockQueryResults = std::vector<MockQueryResult>; G_GNUC_UNUSED static std::ostream& operator<<(std::ostream& os, const MockQueryResults& qrs) { for (auto&& mi : qrs) os << mi.query_match().thread_path << " :: " << mi.message_id().value_or("<none>") << std::endl; return os; } static void calculate_threads(MockQueryResults& qres, bool descending) { calculate_threads_real(qres, descending); } using Expected = std::vector<std::pair<std::string, std::string>>; static void assert_thread_paths(const MockQueryResults& qrs, const Expected& expected) { for (auto&& exp : expected) { auto it = std::find_if(qrs.begin(), qrs.end(), [&](auto&& qr) { return qr.message_id().value_or("") == exp.first || qr.path().value_or("") == exp.first; }); g_assert_true(it != qrs.end()); g_debug("thread-path (%s@%s): expected: '%s'; got '%s'", it->message_id().value_or("<none>").c_str(), it->path().value_or("<none>").c_str(), exp.second.c_str(), it->query_match().thread_path.c_str()); g_assert_cmpstr(exp.second.c_str(), ==, it->query_match().thread_path.c_str()); } } static void test_sort_ascending() { auto results = MockQueryResults{MockQueryResult{"m1", "1", {"m2"}}, MockQueryResult{"m2", "2", {"m3"}}, MockQueryResult{"m3", "3", {}}, MockQueryResult{"m4", "4", {}}}; calculate_threads(results, false); assert_thread_paths(results, {{"m1", "0:0:0"}, {"m2", "0:0"}, {"m3", "0"}, {"m4", "1"}}); } static void test_sort_descending() { auto results = MockQueryResults{MockQueryResult{"m1", "1", {"m2"}}, MockQueryResult{"m2", "2", {"m3"}}, MockQueryResult{"m3", "3", {}}, MockQueryResult{"m4", "4", {}}}; calculate_threads(results, true); assert_thread_paths(results, {{"m1", "1:f:f:z"}, {"m2", "1:f:z"}, {"m3", "1:z"}, {"m4", "0:z"}}); } static void test_id_table_inconsistent() { auto results = MockQueryResults{ MockQueryResult{"m1", "1", {"m2"}}, // 1->2 MockQueryResult{"m2", "2", {"m1"}}, // 2->1 MockQueryResult{"m3", "3", {"m3"}}, // self ref MockQueryResult{"m4", "4", {"m3", "m5"}}, MockQueryResult{"m5", "5", {"m4", "m4"}}, // dup parent }; calculate_threads(results, false); assert_thread_paths(results, { {"m2", "0"}, {"m1", "0:0"}, {"m3", "1"}, {"m5", "1:0"}, {"m4", "1:0:0"}, }); } static void test_dups_dup_last() { MockQueryResult r1{"m1", "1", {}}; r1.query_match().flags |= QueryMatch::Flags::Leader; r1.path_ = "/path1"; MockQueryResult r1_dup{"m1", "1", {}}; r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate; r1_dup.path_ = "/path2"; auto results = MockQueryResults{r1, r1_dup}; calculate_threads(results, false); assert_thread_paths(results, { {"/path1", "0"}, {"/path2", "0:0"}, }); } static void test_dups_dup_first() { // now dup becomes the leader; this will _demote_ // r1. MockQueryResult r1_dup{"m1", "1", {}}; r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate; r1_dup.path_ = "/path1"; MockQueryResult r1{"m1", "1", {}}; r1.query_match().flags |= QueryMatch::Flags::Leader; r1.path_ = "/path2"; auto results = MockQueryResults{r1_dup, r1}; calculate_threads(results, false); assert_thread_paths(results, { {"/path2", "0"}, {"/path1", "0:0"}, }); } static void test_dups_dup_multi() { // now dup becomes the leader; this will _demote_ // r1. MockQueryResult r1_dup1{"m1", "1", {}}; r1_dup1.query_match().flags |= QueryMatch::Flags::Duplicate; r1_dup1.path_ = "/path1"; MockQueryResult r1_dup2{"m1", "1", {}}; r1_dup2.query_match().flags |= QueryMatch::Flags::Duplicate; r1_dup2.path_ = "/path2"; MockQueryResult r1{"m1", "1", {}}; r1.query_match().flags |= QueryMatch::Flags::Leader; r1.path_ = "/path3"; auto results = MockQueryResults{r1_dup1, r1_dup2, r1}; calculate_threads(results, false); assert_thread_paths(results, { {"/path3", "0"}, {"/path1", "0:0"}, {"/path2", "0:1"}, }); } static void test_do_not_prune_root_empty_with_children() { // m7 should not be nuked auto results = MockQueryResults{ MockQueryResult{"x1", "1", {"m7"}}, MockQueryResult{"x2", "2", {"m7"}}, }; calculate_threads(results, false); assert_thread_paths(results, { {"x1", "0:0"}, {"x2", "0:1"}, }); } static void test_prune_root_empty_with_child() { // m7 should be nuked auto results = MockQueryResults{ MockQueryResult{"m1", "1", {"m7"}}, }; calculate_threads(results, false); assert_thread_paths(results, { {"m1", "0"}, }); } static void test_prune_empty_with_children() { // m6 should be nuked auto results = MockQueryResults{ MockQueryResult{"m1", "1", {"m7", "m6"}}, MockQueryResult{"m2", "2", {"m7", "m6"}}, }; calculate_threads(results, false); assert_thread_paths(results, { {"m1", "0:0"}, {"m2", "0:1"}, }); } static void test_thread_info_ascending() { auto results = MockQueryResults{ MockQueryResult{"m1", "5", {}}, MockQueryResult{"m2", "1", {}}, MockQueryResult{"m3", "3", {"m2"}}, MockQueryResult{"m4", "2", {"m2"}}, // orphan siblings MockQueryResult{"m10", "6", {"m9"}}, MockQueryResult{"m11", "7", {"m9"}}, }; calculate_threads(results, false); assert_thread_paths(results, { {"m2", "0"}, // 2 {"m4", "0:0"}, // 2 {"m3", "0:1"}, // 3 {"m1", "1"}, // 5 {"m10", "2:0"}, // 6 {"m11", "2:1"}, // 7 }); g_assert_true(results[0].query_match().has_flag(QueryMatch::Flags::Root)); g_assert_true(results[1].query_match().has_flag(QueryMatch::Flags::Root | QueryMatch::Flags::HasChild)); g_assert_true(results[2].query_match().has_flag(QueryMatch::Flags::Last)); g_assert_true(results[3].query_match().has_flag(QueryMatch::Flags::First)); g_assert_true(results[4].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::First)); g_assert_true( results[5].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::Last)); } static void test_thread_info_descending() { auto results = MockQueryResults{ MockQueryResult{"m1", "5", {}}, MockQueryResult{"m2", "1", {}}, MockQueryResult{"m3", "3", {"m2"}}, MockQueryResult{"m4", "2", {"m2"}}, // orphan siblings MockQueryResult{"m10", "6", {"m9"}}, MockQueryResult{"m11", "7", {"m9"}}, }; calculate_threads(results, true /*descending*/); assert_thread_paths(results, { {"m1", "1:z"}, // 5 {"m2", "2:z"}, // 2 {"m4", "2:f:z"}, // 2 {"m3", "2:e:z"}, // 3 {"m10", "0:f:z"}, // 6 {"m11", "0:e:z"}, // 7 }); g_assert_true(results[0].query_match().has_flag(QueryMatch::Flags::Root)); g_assert_true(results[1].query_match().has_flag(QueryMatch::Flags::Root | QueryMatch::Flags::HasChild)); g_assert_true(results[2].query_match().has_flag(QueryMatch::Flags::Last)); g_assert_true(results[3].query_match().has_flag(QueryMatch::Flags::First)); g_assert_true( results[4].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::Last)); g_assert_true(results[5].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::First)); } int main(int argc, char* argv[]) try { g_test_init(&argc, &argv, NULL); g_test_add_func("/threader/sort/ascending", test_sort_ascending); g_test_add_func("/threader/sort/decending", test_sort_descending); g_test_add_func("/threader/id-table-inconsistent", test_id_table_inconsistent); g_test_add_func("/threader/dups/dup-last", test_dups_dup_last); g_test_add_func("/threader/dups/dup-first", test_dups_dup_first); g_test_add_func("/threader/dups/dup-multi", test_dups_dup_multi); g_test_add_func("/threader/prune/do-not-prune-root-empty-with-children", test_do_not_prune_root_empty_with_children); g_test_add_func("/threader/prune/prune-root-empty-with-child", test_prune_root_empty_with_child); g_test_add_func("/threader/prune/prune-empty-with-children", test_prune_empty_with_children); g_test_add_func("/threader/thread-info/ascending", test_thread_info_ascending); g_test_add_func("/threader/thread-info/descending", test_thread_info_descending); return g_test_run(); } catch (const std::runtime_error& re) { std::cerr << re.what() << "\n"; return 1; } catch (...) { std::cerr << "caught exception\n"; return 1; } #endif /*BUILD_TESTS*/ ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-query-threads.hh�������������������������������������������������������������������0000664�0000000�0000000�00000002562�14523230065�0016532�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2021 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_QUERY_THREADS__ #define MU_QUERY_THREADS__ #include "mu-query-results.hh" namespace Mu { /** * Calculate the threads for these query results; that is, determine the * thread-paths for each message, so we can let Xapian order them in the correct * order. * * Note - threads are sorted chronologically, and the messages below the top * level are always sorted in ascending orde * * @param qres query results * @param descending whether to sort the top-level in descending order */ void calculate_threads(QueryResults& qres, bool descending); } // namespace Mu #endif /*MU_QUERY_THREADS__*/ ����������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-query.cc���������������������������������������������������������������������������0000664�0000000�0000000�00000022717�14523230065�0015074�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2008-2021 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 <mu-query.hh> #include <stdexcept> #include <string> #include <cctype> #include <cstring> #include <sstream> #include <cmath> #include <stdlib.h> #include <xapian.h> #include <glib/gstdio.h> #include "mu-query-results.hh" #include "mu-query-match-deciders.hh" #include "mu-query-threads.hh" #include <mu-xapian.hh> #include "utils/mu-xapian-utils.hh" using namespace Mu; struct Query::Private { Private(const Store& store) : store_{store}, parser_{store_} {} // New // bool calculate_threads (Xapian::Enquire& enq, size maxnum); Xapian::Enquire make_enquire(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags) const; Xapian::Enquire make_related_enquire(const StringSet& thread_ids, Field::Id sortfield_id, QueryFlags qflags) const; Option<QueryResults> run_threaded(QueryResults&& qres, Xapian::Enquire& enq, QueryFlags qflags, size_t max_size) const; Option<QueryResults> run_singular(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags, size_t maxnum) const; Option<QueryResults> run_related(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags, size_t maxnum) const; Option<QueryResults> run(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags, size_t maxnum) const; size_t store_size() const { return store_.database().get_doccount(); } const Store& store_; const Parser parser_; }; Query::Query(const Store& store) : priv_{std::make_unique<Private>(store)} {} Query::Query(Query&& other) = default; Query::~Query() = default; static Xapian::Enquire& sort_enquire(Xapian::Enquire& enq, Field::Id sortfield_id, QueryFlags qflags) { const auto value_no{field_from_id(sortfield_id).value_no()}; enq.set_sort_by_value(value_no, any_of(qflags & QueryFlags::Descending)); return enq; } Xapian::Enquire Query::Private::make_enquire(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags) const { Xapian::Enquire enq{store_.database()}; if (expr.empty() || expr == R"("")") enq.set_query(Xapian::Query::MatchAll); else { WarningVec warns; const auto tree{parser_.parse(expr, warns)}; for (auto&& w : warns) g_warning("query warning: %s", to_string(w).c_str()); enq.set_query(xapian_query(tree)); g_debug("qtree: %s", to_string(tree).c_str()); } sort_enquire(enq, sortfield_id, qflags); return enq; } Xapian::Enquire Query::Private::make_related_enquire(const StringSet& thread_ids, Field::Id sortfield_id, QueryFlags qflags) const { Xapian::Enquire enq{store_.database()}; std::vector<Xapian::Query> qvec; for (auto&& t : thread_ids) qvec.emplace_back(field_from_id(Field::Id::ThreadId).xapian_term(t)); Xapian::Query qr{Xapian::Query::OP_OR, qvec.begin(), qvec.end()}; enq.set_query(qr); sort_enquire(enq, sortfield_id, qflags); return enq; } struct ThreadKeyMaker : public Xapian::KeyMaker { ThreadKeyMaker(const QueryMatches& matches) : match_info_(matches) {} std::string operator()(const Xapian::Document& doc) const override { const auto it{match_info_.find(doc.get_docid())}; return (it == match_info_.end()) ? "" : it->second.thread_path; } const QueryMatches& match_info_; }; Option<QueryResults> Query::Private::run_threaded(QueryResults&& qres, Xapian::Enquire& enq, QueryFlags qflags, size_t maxnum) const { const auto descending{any_of(qflags & QueryFlags::Descending)}; calculate_threads(qres, descending); ThreadKeyMaker key_maker{qres.query_matches()}; enq.set_sort_by_key(&key_maker, descending); DeciderInfo minfo; minfo.matches = qres.query_matches(); auto mset{enq.get_mset(0, maxnum, {}, make_thread_decider(qflags, minfo).get())}; mset.fetch(); return QueryResults{mset, std::move(qres.query_matches())}; } Option<QueryResults> Query::Private::run_singular(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags, size_t maxnum) const { // i.e. a query _without_ related messages, but still possibly // with threading. // // In the threading case, the sortfield-id is ignored, we always sort by // date (since threading the threading results are always by date.) const auto singular_qflags{qflags | QueryFlags::Leader}; const auto threading{any_of(qflags & QueryFlags::Threading)}; DeciderInfo minfo{}; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wextra" auto enq{make_enquire(expr, threading ? Field::Id::Date : sortfield_id, qflags)}; #pragma GCC diagnostic ignored "-Wswitch-default" #pragma GCC diagnostic pop auto mset{enq.get_mset(0, maxnum, {}, make_leader_decider(singular_qflags, minfo).get())}; mset.fetch(); auto qres{QueryResults{mset, std::move(minfo.matches)}}; return threading ? run_threaded(std::move(qres), enq, qflags, maxnum) : qres; } static Option<std::string> opt_string(const Xapian::Document& doc, Field::Id id) noexcept { const auto value_no{field_from_id(id).value_no()}; std::string val = xapian_try([&] { return doc.get_value(value_no); }, std::string{""}); if (val.empty()) return Nothing; else return Some(std::move(val)); } Option<QueryResults> Query::Private::run_related(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags, size_t maxnum) const { // i.e. a query _with_ related messages and possibly with threading. // // In the threading case, the sortfield-id is ignored, we always sort by // date (since threading the threading results are always by date.); // moreover, in either threaded or non-threaded case, we sort the first // ("leader") query by date, i.e, we prefer the newest or oldest // (descending) messages. const auto leader_qflags{QueryFlags::Leader | qflags}; const auto threading{any_of(qflags & QueryFlags::Threading)}; // Run our first, "leader" query DeciderInfo minfo{}; auto enq{make_enquire(expr, Field::Id::Date, leader_qflags)}; const auto mset{ enq.get_mset(0, maxnum, {}, make_leader_decider(leader_qflags, minfo).get())}; // Gather the thread-ids we found mset.fetch(); for (auto it = mset.begin(); it != mset.end(); ++it) { auto thread_id{opt_string(it.get_document(), Field::Id::ThreadId)}; if (thread_id) minfo.thread_ids.emplace(std::move(*thread_id)); } // Now, determine the "related query". // // In the threaded-case, we search among _all_ messages, since complete // threads are preferred; no need to sort in that case since the search // is unlimited and the sorting happens during threading. auto r_enq = std::invoke([&]{ if (threading) return make_related_enquire(minfo.thread_ids, Field::Id::Date, qflags ); else return make_related_enquire(minfo.thread_ids, sortfield_id, qflags); }); const auto r_mset{r_enq.get_mset(0, threading ? store_size() : maxnum, {}, make_related_decider(qflags, minfo).get())}; auto qres{QueryResults{r_mset, std::move(minfo.matches)}}; return threading ? run_threaded(std::move(qres), r_enq, qflags, maxnum) : qres; } Option<QueryResults> Query::Private::run(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags, size_t maxnum) const { const auto eff_maxnum{maxnum == 0 ? store_size() : maxnum}; if (any_of(qflags & QueryFlags::IncludeRelated)) return run_related(expr, sortfield_id, qflags, eff_maxnum); else return run_singular(expr, sortfield_id, qflags, eff_maxnum); } Result<QueryResults> Query::run(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags, size_t maxnum) const { // some flags are for internal use only. g_return_val_if_fail(none_of(qflags & QueryFlags::Leader), Err(Error::Code::InvalidArgument, "cannot pass Leader flag")); StopWatch sw{format( "ran query '%s'; related: %s; threads: %s; max-size: %zu", expr.c_str(), any_of(qflags & QueryFlags::IncludeRelated) ? "yes" : "no", any_of(qflags & QueryFlags::Threading) ? "yes" : "no", maxnum)}; return xapian_try_result([&]{ if (auto&& res = priv_->run(expr, sortfield_id, qflags, maxnum); res) return Result<QueryResults>(Ok(std::move(res.value()))); else return Result<QueryResults>(Err(Error::Code::Query, "failed to run query")); }); } size_t Query::count(const std::string& expr) const { return xapian_try( [&] { const auto enq{priv_->make_enquire(expr, {}, {})}; auto mset{enq.get_mset(0, priv_->store_size())}; mset.fetch(); return mset.size(); }, 0); } /* LCOV_EXCL_START*/ std::string Query::parse(const std::string& expr, bool xapian) const { WarningVec warns; const auto tree{priv_->parser_.parse(expr, warns)}; for (auto&& w : warns) g_warning("query warning: %s", to_string(w).c_str()); if (xapian) return xapian_query(tree).get_description(); else return to_string(tree); } /* LCOV_EXCL_STOP*/ �������������������������������������������������mu-1.10.8/lib/mu-query.hh���������������������������������������������������������������������������0000664�0000000�0000000�00000004763�14523230065�0015107�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2008-2021 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_HH__ #define __MU_QUERY_HH__ #include <memory> #include <glib.h> #include <mu-store.hh> #include <mu-query-results.hh> #include <utils/mu-utils.hh> #include <utils/mu-option.hh> #include <utils/mu-result.hh> #include <message/mu-message.hh> namespace Mu { class Query { public: /** * Run a query on the store * * @param expr the search expression * @param sortfield_id the sortfield-id. Default to Date * @param flags query flags * @param maxnum maximum number of results to return. 0 for 'no limit' * * @return the query-results or an error */ Result<QueryResults> run(const std::string& expr, Field::Id sortfield_id = Field::Id::Date, QueryFlags flags = QueryFlags::None, size_t maxnum = 0) const; /** * run a Xapian query to count the number of matches; for the syntax, please * refer to the mu-query manpage * * @param expr the search expression; use "" to match all messages * * @return the number of matches */ size_t count(const std::string& expr = "") const; /** * For debugging, get the internal string representation of the parsed * query * * @param expr a xapian search expression * @param xapian if true, show Xapian's internal representation, * otherwise, mu's. * @return the string representation of the query */ std::string parse(const std::string& expr, bool xapian) const; private: friend class Store; /** * Construct a new Query instance. * * @param store a MuStore object */ Query(const Store& store); /** * DTOR * */ ~Query(); /** * Move CTOR * * @param other */ Query(Query&& other); struct Private; std::unique_ptr<Private> priv_; }; } // namespace Mu #endif /*__MU_QUERY_HH__*/ �������������mu-1.10.8/lib/mu-script.cc��������������������������������������������������������������������������0000664�0000000�0000000�00000007231�14523230065�0015225�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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 "mu-script.hh" #ifdef BUILD_GUILE #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wredundant-decls" #include <libguile.h> #pragma GCC diagnostic pop #endif /*BUILD_GUILE*/ #include "mu/mu-options.hh" #include "utils/mu-utils.hh" #include "utils/mu-option.hh" #include <fstream> #include <iostream> using namespace Mu; static std::string get_name(const std::string& path) { auto pos = path.find_last_of("/"); if (pos == std::string::npos) return path; auto name = path.substr(pos + 1); pos = name.find_last_of("."); if (pos == std::string::npos) return name; return name.substr(0, pos); } static Mu::Option<Mu::ScriptInfo> get_info(std::string&& path, const std::string& prefix) { std::ifstream file{path}; if (!file.is_open()) { g_warning ("failed to open %s", path.c_str()); return Nothing; } Mu::ScriptInfo info{}; info.path = path; info.name = get_name(path); std::string line; while (std::getline(file, line)) { if (line.find(prefix) != 0) continue; line = line.substr(prefix.length()); if (info.oneline.empty()) info.oneline = line; else info.description += line; } // std::cerr << "ONELINE: " << info.oneline << '\n'; // std::cerr << "DESCR : " << info.description << '\n'; return info; } static void script_infos_in_dir(const std::string& scriptdir, Mu::ScriptInfos& infos) { DIR *dir = opendir(scriptdir.c_str()); if (!dir) { g_debug("failed to open '%s': %s", scriptdir.c_str(), g_strerror(errno)); return; } const std::string ext{".scm"}; struct dirent *dentry; while ((dentry = readdir(dir))) { if (!g_str_has_suffix(dentry->d_name, ext.c_str())) continue; auto&& info = get_info(scriptdir + "/" + dentry->d_name, ";; INFO: "); if (!info) continue; infos.emplace_back(std::move(*info)); } closedir(dir); /* ignore error checking... */ } Mu::ScriptInfos Mu::script_infos(const Mu::ScriptPaths& paths) { /* create a list of names, paths */ ScriptInfos infos; for (auto&& dir: paths) { script_infos_in_dir(dir, infos); } std::sort(infos.begin(), infos.end(), [](auto&& i1, auto&& i2) { return i1.name < i2.name; }); return infos; } Result<void> Mu::run_script(const std::string& path, const std::vector<std::string>& args) { #ifndef BUILD_GUILE return Err(Error::Code::Script, "guile script support is not available"); #else std::string mainargs; for (auto&& arg: args) mainargs += format("%s\"%s\"", mainargs.empty() ? "" : " ", arg.c_str()); auto expr = format("(main '(\"%s\" %s))", get_name(path).c_str(), mainargs.c_str()); std::vector<const char*> argv = { GUILE_BINARY, "-l", path.c_str(), "-c", expr.c_str(), }; /* does not return */ scm_boot_guile(argv.size(), const_cast<char**>(argv.data()), [](void *closure, int argc, char **argv) { scm_shell(argc, argv); }, NULL); return Ok(); #endif /*BUILD_GUILE*/ } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-script.hh��������������������������������������������������������������������������0000664�0000000�0000000�00000003415�14523230065�0015237�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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_HH__ #define MU_SCRIPT_HH__ #include <string> #include <vector> #include <utils/mu-result.hh> namespace Mu { /** * Information about a script. * */ struct ScriptInfo { std::string name; /**< Name of script */ std::string path; /**< Full path to script */ std::string oneline; /**< One-line description */ std::string description; /**< More help */ }; /// Sequence of script infos. using ScriptInfos = std::vector<ScriptInfo>; /** * Get information about the available scripts * * @return infos */ using ScriptPaths = std::vector<std::string>; ScriptInfos script_infos(const ScriptPaths& paths); /** * Run some specific script * * @param path full path to the scripts * @param args argument vector to pass to the script * * @return Ok() or some error; however, note that this does not return after succesfully * starting a script. */ Result<void> run_script(const std::string& path, const std::vector<std::string>& args); } // namepace Mu #endif /* MU_SCRIPT_HH__ */ ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-server.cc��������������������������������������������������������������������������0000664�0000000�0000000�00000100024�14523230065�0015221�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2020-2022 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 "message/mu-message.hh" #include "mu-server.hh" #include <iostream> #include <string> #include <algorithm> #include <atomic> #include <thread> #include <mutex> #include <functional> #include <cstring> #include <glib.h> #include <glib/gprintf.h> #include <unistd.h> #include "mu-maildir.hh" #include "mu-query.hh" #include "index/mu-indexer.hh" #include "mu-store.hh" #include "utils/mu-utils.hh" #include "utils/mu-option.hh" #include "utils/mu-command-handler.hh" #include "utils/mu-readline.hh" using namespace Mu; /// @brief object to manage the server-context for all commands. struct Server::Private { Private(Store& store, Output output) : store_{store}, output_{output}, command_handler_{make_command_map()}, keep_going_{true} {} ~Private() { indexer().stop(); if (index_thread_.joinable()) index_thread_.join(); } // // construction helpers // CommandHandler::CommandInfoMap make_command_map(); // // acccessors Store& store() { return store_; } const Store& store() const { return store_; } Indexer& indexer() { return store().indexer(); } //CommandMap& command_map() const { return command_map_; } // // invoke // bool invoke(const std::string& expr) noexcept; // // output // void output_sexp(const Sexp& sexp, Server::OutputFlags flags = {}) const { if (output_) output_(sexp, flags); } size_t output_results(const QueryResults& qres, size_t batch_size) const; // // handlers for various commands. // void add_handler(const Command& cmd); void compose_handler(const Command& cmd); void contacts_handler(const Command& cmd); void find_handler(const Command& cmd); void help_handler(const Command& cmd); void index_handler(const Command& cmd); void move_handler(const Command& cmd); void mkdir_handler(const Command& cmd); void ping_handler(const Command& cmd); void queries_handler(const Command& cmd); void quit_handler(const Command& cmd); void remove_handler(const Command& cmd); void sent_handler(const Command& cmd); void view_handler(const Command& cmd); private: // helpers Sexp build_message_sexp(const Message& msg, Store::Id docid, const Option<QueryMatch&> qm) const; void move_docid(Store::Id docid, Option<std::string> flagstr, bool new_name, bool no_view); void perform_move(Store::Id docid, const Message& msg, const std::string& maildirarg, Flags flags, bool new_name, bool no_view); void view_mark_as_read(Store::Id docid, Message&& msg, bool rename); Store& store_; Server::Output output_; const CommandHandler command_handler_; std::atomic<bool> keep_going_{}; std::thread index_thread_; }; static Sexp build_metadata(const QueryMatch& qmatch) { const auto td{::atoi(qmatch.thread_date.c_str())}; auto mdata = Sexp().put_props(":path", qmatch.thread_path, ":level", qmatch.thread_level, ":date", qmatch.thread_date, ":data-tstamp", Sexp().add(static_cast<unsigned>(td >> 16), static_cast<unsigned>(td & 0xffff), 0)); if (qmatch.has_flag(QueryMatch::Flags::Root)) mdata.put_props(":root", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Related)) mdata.put_props(":related", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::First)) mdata.put_props(":first-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Last)) mdata.put_props(":last-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Orphan)) mdata.put_props(":orphan", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Duplicate)) mdata.put_props(":duplicate", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::HasChild)) mdata.put_props(":has-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject)) mdata.put_props(":thread-subject", Sexp::t()); return mdata; } /* * A message here consists of a message s-expression with optionally a :docid * and/or :meta expression added. */ Sexp Server::Private::build_message_sexp(const Message& msg, Store::Id docid, const Option<QueryMatch&> qm) const { Sexp sexp{msg.sexp()}; // copy if (docid != 0) sexp.put_props(":docid", docid); if (qm) sexp.put_props(":meta", build_metadata(*qm)); return sexp; } CommandHandler::CommandInfoMap Server::Private::make_command_map() { CommandHandler::CommandInfoMap cmap; using CommandInfo = CommandHandler::CommandInfo; using ArgMap = CommandHandler::ArgMap; using ArgInfo = CommandHandler::ArgInfo; using Type = Sexp::Type; using Type = Sexp::Type; 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(params); }}); cmap.emplace( "compose", CommandInfo{ ArgMap{ {":type", ArgInfo{Type::Symbol, true, "type of composition: reply/forward/edit/resend/new"}}, {":docid", ArgInfo{Type::Number, false, "document id of parent-message, if any"}}, {":decrypt", ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)"}}}, "compose a new message", [&](const auto& params) { compose_handler(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"}}, {":maxnum", ArgInfo{Type::Number, false, "max number of contacts to return"}}}, "get contact information", [&](const auto& params) { contacts_handler(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"}}, {":batch-size", ArgInfo{Type::Number, false, "batch size for result"}}, {":maxnum", ArgInfo{Type::Number, 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(params); }}); cmap.emplace( "help", CommandInfo{ ArgMap{{":command", ArgInfo{Type::Symbol, false, "command to get information for"}}, {":full", ArgInfo{Type::Symbol, false, "show full descriptions"}}}, "get information about one or all commands", [&](const auto& params) { help_handler(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(params); }}); cmap.emplace( "mkdir", CommandInfo{ ArgMap{ {":path", ArgInfo{Type::String, true, "location for the new maildir"}}, {":update", ArgInfo{Type::Symbol, false, "whether to send an update after creating"}} }, "create a new maildir", [&](const auto& params) { mkdir_handler(params); }}); cmap.emplace( "move", CommandInfo{ ArgMap{ {":docid", ArgInfo{Type::Number, 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(params); }}); cmap.emplace( "ping", CommandInfo{ ArgMap{}, "ping the mu-server and get server information in the response", [&](const auto& params) { ping_handler(params); }}); cmap.emplace( "queries", CommandInfo{ ArgMap{ {":queries", ArgInfo{Type::List, false, "queries for which to get read/unread numbers"}}, }, "get unread/totals information for a list of queries", [&](const auto& params) { queries_handler(params); }}); cmap.emplace("quit", CommandInfo{{}, "quit the mu server", [&](const auto& params) { quit_handler(params); }}); cmap.emplace( "remove", CommandInfo{ ArgMap{{":docid", ArgInfo{Type::Number, true, "document-id for the message to remove"}}}, "remove a message from filesystem and database", [&](const auto& params) { remove_handler(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(params); }}); cmap.emplace( "view", CommandInfo{ArgMap{ {":docid", ArgInfo{Type::Number, false, "document-id"}}, {":msgid", ArgInfo{Type::String, false, "message-id"}}, {":path", ArgInfo{Type::String, false, "message filesystem path"}}, {":mark-as-read", ArgInfo{Type::Symbol, false, "mark message as read (if not already)"}}, {":rename", ArgInfo{Type::Symbol, false, "change filename when moving"}}, }, "view a message. exactly one of docid/msgid/path must be specified", [&](const auto& params) { view_handler(params); }}); return cmap; } G_GNUC_PRINTF(2, 3) static Sexp make_error(Error::Code code, const char* frm, ...) { va_list ap; va_start(ap, frm); auto err = Sexp().put_props( ":error", Error::error_number(code), ":message", vformat(frm, ap)); va_end(ap); return err; } bool Server::Private::invoke(const std::string& expr) noexcept { if (!keep_going_) return false; try { auto cmd{Command::make_parse(std::string{expr})}; if (!cmd) throw cmd.error(); auto res = command_handler_.invoke(*cmd); if (!res) throw res.error(); } catch (const Mu::Error& me) { output_sexp(make_error(me.code(), "%s", me.what())); keep_going_ = true; } catch (const Xapian::Error& xerr) { output_sexp(make_error(Error::Code::Internal, "xapian error: %s: %s", xerr.get_type(), xerr.get_description().c_str())); keep_going_ = false; } catch (const std::runtime_error& re) { output_sexp(make_error(Error::Code::Internal, "caught exception: %s", re.what())); keep_going_ = false; } catch (...) { output_sexp(make_error(Error::Code::Internal, "something went wrong: quiting")); keep_going_ = false; } return keep_going_; } /* '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) */ void Server::Private::add_handler(const Command& cmd) { auto path{cmd.string_arg(":path")}; const auto docid_res{store().add_message(*path)}; if (!docid_res) throw docid_res.error(); const auto docid{docid_res.value()}; output_sexp(Sexp().put_props(":info", "add"_sym, ":path", *path, ":docid", docid)); auto msg_res{store().find_message(docid)}; if (!msg_res) throw Error(Error::Code::Store, "failed to get message at %s (docid=%u): %s", path->c_str(), docid); output_sexp(Sexp().put_props(":update", build_message_sexp(msg_res.value(), docid, {}))); } /* '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 <type> [:original <original-msg>] [:include] ) * message (detals: see code below) * * Note ':include' t or nil determines whether to include attachments */ static Option<Sexp> maybe_add_attachment(Message& message, const MessagePart& part, size_t index) { if (!part.is_attachment()) return Nothing; const auto cache_path{message.cache_path(index)}; if (!cache_path) throw cache_path.error(); const auto cooked_name{part.cooked_filename()}; const auto fname{format("%s/%s", cache_path->c_str(), cooked_name.value_or("part").c_str())}; const auto res = part.to_file(fname, true); if (!res) throw res.error(); Sexp pi; if (auto cdescr = part.content_description(); cdescr) pi.put_props(":description", *cdescr); else if (cooked_name) pi.put_props(":description", cooked_name.value()); pi.put_props(":file-name", fname, ":mime-type", part.mime_type().value_or("application/octet-stream")); return Some(std::move(pi)); } void Server::Private::compose_handler(const Command& cmd) { const auto ctype = cmd.symbol_arg(":type").value_or("<error>"); auto comp_lst = Sexp().put_props(":compose", Sexp::Symbol(ctype)); if (ctype == "reply" || ctype == "forward" || ctype == "edit" || ctype == "resend") { const unsigned docid{static_cast<unsigned>(cmd.number_arg(":docid").value_or(0))}; auto msg{store().find_message(docid)}; if (!msg) throw Error{Error::Code::Store, "failed to get message %u", docid}; comp_lst.put_props(":original", build_message_sexp(msg.value(), docid, {})); if (ctype == "forward") { // when forwarding, attach any attachment in the orig size_t index{}; Sexp attseq; for (auto&& part: msg->parts()) { if (auto attsexp = maybe_add_attachment( *msg, part, index); attsexp) { attseq.add(std::move(*attsexp)); ++index; } } if (!attseq.empty()) { comp_lst.put_props(":include", std::move(attseq), ":cache-path", *msg->cache_path()); } } } else if (ctype != "new") throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'", ctype.c_str()); output_sexp(comp_lst); } void Server::Private::contacts_handler(const Command& cmd) { const auto personal = cmd.boolean_arg(":personal"); const auto afterstr = cmd.string_arg(":after").value_or(""); const auto tstampstr = cmd.string_arg(":tstamp").value_or(""); const auto maxnum = cmd.number_arg(":maxnum").value_or(0 /*unlimited*/); const auto after{afterstr.empty() ? 0 : parse_date_time(afterstr, true).value_or(0)}; const auto tstamp = g_ascii_strtoll(tstampstr.c_str(), NULL, 10); g_debug("find %s contacts last seen >= %s (tstamp: %zu)", personal ? "personal" : "any", time_to_string("%c", after).c_str(), static_cast<size_t>(tstamp)); auto n{0}; Sexp contacts; store().contacts_cache().for_each([&](const Contact& ci) { /* since the last time we got some contacts */ if (tstamp > ci.tstamp) return true; /* (maybe) only include 'personal' contacts */ if (personal && !ci.personal) return true; /* only include newer-than-x contacts */ if (after > ci.message_date) return true; n++; contacts.add(ci.display_name()); return maxnum == 0 || n < maxnum; }); Sexp seq; seq.put_props(":contacts", contacts, ":tstamp", format("%" G_GINT64_FORMAT, g_get_monotonic_time())); /* dump the contacts cache as a giant sexp */ g_debug("sending %d of %zu contact(s)", n, store().contacts_cache().size()); output_sexp(seq, Server::OutputFlags::SplitList); } /* get a *list* of all messages with the given message id */ static std::vector<Store::Id> docids_for_msgid(const Store& store, const std::string& msgid, size_t max = 100) { if (msgid.size() > MaxTermLength) { throw Error(Error::Code::InvalidArgument, "invalid message-id '%s'", msgid.c_str()); } else if (msgid.empty()) return {}; const auto xprefix{field_from_id(Field::Id::MessageId).shortcut}; /*XXX this is a bit dodgy */ auto tmp{g_ascii_strdown(msgid.c_str(), -1)}; auto expr{g_strdup_printf("%c:%s", xprefix, tmp)}; g_free(tmp); GError* gerr{}; std::lock_guard l{store.lock()}; const auto res{store.run_query(expr, {}, QueryFlags::None, max)}; g_free(expr); if (!res) throw Error(Error::Code::Store, &gerr, "failed to run message-id-query: %s", res.error().what()); else if (res->empty()) throw Error(Error::Code::NotFound, "could not find message(s) for msgid %s", msgid.c_str()); std::vector<Store::Id> docids{}; for (auto&& mi : *res) docids.emplace_back(mi.doc_id()); 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(const Store& store, Store::Id docid) { auto msg{store.find_message(docid)}; if (!msg) throw Error(Error::Code::Store, "could not get message from store"); if (auto path{msg->path()}; path.empty()) throw Error(Error::Code::Store, "could not get path for message %u", docid); else return path; } static std::vector<Store::Id> determine_docids(const Store& store, const Command& cmd) { auto docid{cmd.number_arg(":docid").value_or(0)}; const auto msgid{cmd.string_arg(":msgid").value_or("")}; if ((docid == 0) == msgid.empty()) throw Error(Error::Code::InvalidArgument, "precisely one of docid and msgid must be specified"); if (docid != 0) return {static_cast<Store::Id>(docid)}; else return docids_for_msgid(store, msgid.c_str()); } size_t Server::Private::output_results(const QueryResults& qres, size_t batch_size) const { size_t n{}; Sexp headers; const auto output_batch = [&](Sexp&& hdrs) { Sexp batch; batch.put_props(":headers", std::move(hdrs)); output_sexp(batch); }; for (auto&& mi : qres) { auto msg{mi.message()}; if (!msg) continue; ++n; // construct sexp for a single header. auto qm{mi.query_match()}; auto msgsexp{build_message_sexp(*msg, mi.doc_id(), qm)}; headers.add(std::move(msgsexp)); // we output up-to-batch-size lists of messages. It's much // faster (on the emacs side) to handle such batches than single // headers. if (headers.size() % batch_size == 0) { output_batch(std::move(headers)); headers.clear(); }; } // remaining. if (!headers.empty()) output_batch(std::move(headers)); return n; } void Server::Private::find_handler(const Command& cmd) { const auto q{cmd.string_arg(":query").value_or("")}; const auto threads{cmd.boolean_arg(":threads")}; // perhaps let mu4e set this as frame-lines of the appropriate frame. const auto batch_size{cmd.number_arg(":batch-size").value_or(110)}; const auto descending{cmd.boolean_arg(":descending")}; const auto maxnum{cmd.number_arg(":maxnum").value_or(-1) /*unlimited*/}; const auto skip_dups{cmd.boolean_arg(":skip-dups")}; const auto include_related{cmd.boolean_arg(":include-related")}; // complicated! auto sort_field_id = std::invoke([&]()->Field::Id { if (const auto arg = cmd.symbol_arg(":sortfield"); !arg) return Field::Id::Date; else if (arg->length() < 2) throw Error{Error::Code::InvalidArgument, "invalid sort field '%s'", arg->c_str()}; else if (const auto field{field_from_name(arg->substr(1))}; !field) throw Error{Error::Code::InvalidArgument, "invalid sort field '%s'", arg->c_str()}; else return field->id; }); if (batch_size < 1) throw Error{Error::Code::InvalidArgument, "invalid batch-size %d", batch_size}; auto qflags{QueryFlags::SkipUnreadable}; // don't show unreadables. if (descending) qflags |= QueryFlags::Descending; if (skip_dups) qflags |= QueryFlags::SkipDuplicates; if (include_related) qflags |= QueryFlags::IncludeRelated; if (threads) qflags |= QueryFlags::Threading; StopWatch sw{format("%s (indexing: %s)", __func__, indexer().is_running() ? "yes" : "no")}; std::lock_guard l{store_.lock()}; auto qres{store_.run_query(q, sort_field_id, qflags, maxnum)}; if (!qres) throw Error(Error::Code::Query, "failed to run query: %s", qres.error().what()); /* 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. */ output_sexp(Sexp().put_props(":erase", Sexp::t())); const auto foundnum{output_results(*qres, static_cast<size_t>(batch_size))}; output_sexp(Sexp().put_props(":found", foundnum)); } void Server::Private::help_handler(const Command& cmd) { const auto command{cmd.symbol_arg(":command").value_or("")}; const auto full{cmd.bool_arg(":full").value_or(!command.empty())}; auto&& info_map{command_handler_.info_map()}; if (command.empty()) { std::cout << ";; Commands are single-line s-expressions of the form\n" << ";; (<command-name> :param1 val1 :param2 val2 ...)\n" << ";; For instance:\n;; (help :command mkdir)\n" << ";; to get detailed information about the 'mkdir' command\n;;\n"; std::cout << ";; The following commands are available:\n\n"; } std::vector<std::string> names; for (auto&& name_cmd: info_map) names.emplace_back(name_cmd.first); std::sort(names.begin(), names.end()); for (auto&& name : names) { const auto& info{info_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 Sexp get_stats(const Indexer::Progress& stats, const std::string& state) { Sexp sexp; sexp.put_props( ":info", "index"_sym, ":status", Sexp::Symbol(state), ":checked", static_cast<int>(stats.checked), ":updated", static_cast<int>(stats.updated), ":cleaned-up", static_cast<int>(stats.removed)); return sexp; } void Server::Private::index_handler(const Command& cmd) { Mu::Indexer::Config conf{}; conf.cleanup = cmd.boolean_arg(":cleanup"); conf.lazy_check = cmd.boolean_arg(":lazy-check"); // ignore .noupdate with an empty store. conf.ignore_noupdate = store().empty(); indexer().stop(); if (index_thread_.joinable()) index_thread_.join(); // start a background track. index_thread_ = std::thread([this, conf = std::move(conf)] { StopWatch sw{"indexing"}; indexer().start(conf); while (indexer().is_running()) { std::this_thread::sleep_for(std::chrono::milliseconds(2000)); output_sexp(get_stats(indexer().progress(), "running"), Server::OutputFlags::Flush); } output_sexp(get_stats(indexer().progress(), "complete"), Server::OutputFlags::Flush); store().commit(); /* ensure on-disk database is updated, too */ }); } void Server::Private::mkdir_handler(const Command& cmd) { const auto path{cmd.string_arg(":path").value_or("<error>")}; const auto update{cmd.boolean_arg(":update")}; if (auto&& res = maildir_mkdir(path, 0755, false); !res) throw res.error(); /* mu4e does a lot of opportunistic 'mkdir', only send it updates when * requested */ if (update) output_sexp(Sexp().put_props(":info", "mkdir", ":message", format("%s has been created", path.c_str()))); } void Server::Private::perform_move(Store::Id docid, const Message& msg, const std::string& maildirarg, Flags flags, bool new_name, bool no_view) { bool different_mdir{}; auto maildir{maildirarg}; if (maildir.empty()) { maildir = msg.maildir(); different_mdir = false; } else /* are we moving to a different mdir, or is it just flags? */ different_mdir = maildir != msg.maildir(); Store::MoveOptions move_opts{Store::MoveOptions::DupFlags}; if (new_name) move_opts |= Store::MoveOptions::ChangeName; /* note: we get back _all_ the messages that changed; the first is the * primary mover; the rest (if present) are any dups affected */ const auto idmsgvec{store().move_message(docid, maildir, flags, move_opts)}; if (!idmsgvec) throw idmsgvec.error(); for (auto&&[id, msg]: *idmsgvec) { Sexp sexp{":update"_sym, build_message_sexp(idmsgvec->at(0).second, id, {})}; /* note, the :move t thing is a hint to the frontend that it * could remove the particular header */ if (different_mdir) sexp.put_props(":move", Sexp::t()); if (!no_view && id == docid) sexp.put_props(":maybe-view", Sexp::t()); output_sexp(std::move(sexp)); } } static Flags calculate_message_flags(const Message& msg, Option<std::string> flagopt) { const auto flags = std::invoke([&]()->Option<Flags>{ if (!flagopt) return msg.flags(); else return flags_from_expr(*flagopt, msg.flags()); }); if (!flags) throw Error{Error::Code::InvalidArgument, "invalid flags '%s'", flagopt.value_or("").c_str()}; else return flags.value(); } void Server::Private::move_docid(Store::Id docid, Option<std::string> flagopt, bool new_name, bool no_view) { if (docid == Store::InvalidId) throw Error{Error::Code::InvalidArgument, "invalid docid"}; auto msg{store_.find_message(docid)}; if (!msg) throw Error{Error::Code::Store, "failed to get message from store"}; const auto flags = calculate_message_flags(msg.value(), flagopt); perform_move(docid, *msg, "", flags, new_name, no_view); } /* * '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. */ void Server::Private::move_handler(const Command& cmd) { auto maildir{cmd.string_arg(":maildir").value_or("")}; const auto flagopt{cmd.string_arg(":flags")}; const auto rename{cmd.boolean_arg(":rename")}; const auto no_view{cmd.boolean_arg(":noupdate")}; const auto docids{determine_docids(store_, cmd)}; if (docids.size() > 1) { if (!maildir.empty()) // ie. duplicate message-ids. throw Mu::Error{Error::Code::Store, "cannot move multiple messages at the same time"}; // multi. for (auto&& docid : docids) move_docid(docid, flagopt, rename, no_view); return; } const auto docid{docids.at(0)}; auto msg = store().find_message(docid) .or_else([&]{throw Error{Error::Code::InvalidArgument, "cannot find message %u", docid};}).value(); /* if maildir was not specified, take the current one */ if (maildir.empty()) maildir = msg.maildir(); /* determine the real target flags, which come from the flags-parameter * we received (ie., flagstr), if any, plus the existing message * flags. */ const auto flags = calculate_message_flags(msg, flagopt); perform_move(docid, msg, maildir, flags, rename, no_view); } void Server::Private::ping_handler(const Command& cmd) { const auto storecount{store().size()}; if (storecount == (unsigned)-1) throw Error{Error::Code::Store, "failed to read store"}; Sexp addrs; for (auto&& addr : store().properties().personal_addresses) addrs.add(addr); output_sexp(Sexp() .put_props(":pong", "mu") .put_props(":props", Sexp().put_props( ":version", VERSION, ":personal-addresses", std::move(addrs), ":database-path", store().properties().database_path, ":root-maildir", store().properties().root_maildir, ":doccount", storecount))); } void Server::Private::queries_handler(const Command& cmd) { const auto queries{cmd.string_vec_arg(":queries") .value_or(std::vector<std::string>{})}; Sexp qresults; for (auto&& q : queries) { const auto count{store_.count_query(q)}; const auto unreadq{format("flag:unread AND (%s)", q.c_str())}; const auto unread{store_.count_query(unreadq)}; qresults.add(Sexp().put_props(":query", q, ":count", count, ":unread", unread)); } output_sexp(Sexp(":queries"_sym, std::move(qresults))); } void Server::Private::quit_handler(const Command& cmd) { keep_going_ = false; } void Server::Private::remove_handler(const Command& cmd) { const auto docid{cmd.number_arg(":docid").value_or(0)}; const auto path{path_from_docid(store(), docid)}; if (::unlink(path.c_str()) != 0 && errno != ENOENT) throw Error(Error::Code::File, "could not delete %s: %s", path.c_str(), g_strerror(errno)); if (!store().remove_message(path)) g_warning("failed to remove message @ %s (%d) from store", path.c_str(), docid); output_sexp(Sexp().put_props(":remove", docid)); // act as if it worked. } void Server::Private::sent_handler(const Command& cmd) { const auto path{cmd.string_arg(":path").value_or("")}; const auto docid = store().add_message(path); if (!docid) throw Error{Error::Code::Store, "failed to add path: %s", docid.error().what()}; output_sexp(Sexp().put_props( ":sent", Sexp::t(), ":path", path, ":docid", docid.value())); } void Server::Private::view_mark_as_read(Store::Id docid, Message&& msg, bool rename) { auto move_res = std::invoke([&]()->Result<Store::IdMessageVec> { const auto newflags{flags_from_delta_expr("+S-u-N", msg.flags())}; if (!newflags || msg.flags() == *newflags) { /* case 1: message was already read; do nothing */ Store::IdMessageVec idmvec; idmvec.emplace_back(docid, std::move(msg)); return idmvec; } else { /* case 2: move message (and possibly dups) */ Store::MoveOptions move_opts{Store::MoveOptions::DupFlags}; if (rename) move_opts |= Store::MoveOptions::ChangeName; return store().move_message(docid, {}, newflags, move_opts); } }); if (!move_res) throw move_res.error(); for (auto&& [id, msg]: move_res.value()) output_sexp(Sexp{id == docid ? ":view"_sym : ":update"_sym, build_message_sexp(msg, id, {})}); } void Server::Private::view_handler(const Command& cmd) { StopWatch sw{format("%s (indexing: %s)", __func__, indexer().is_running() ? "yes" : "no")}; const auto mark_as_read{cmd.boolean_arg(":mark-as-read")}; /* for now, do _not_ rename, as it seems to confuse mbsync */ const auto rename{false}; //const auto rename{get_bool_or(params, ":rename")}; const auto docids{determine_docids(store(), cmd)}; if (docids.empty()) throw Error{Error::Code::Store, "failed to find message for view"}; const auto docid{docids.at(0)}; auto msg = store().find_message(docid) .or_else([]{throw Error{Error::Code::Store, "failed to find message for view"};}).value(); /* if the message should not be marked-as-read, we're done. */ if (!mark_as_read) output_sexp(Sexp().put_props(":view", build_message_sexp(msg, docid, {}))); else view_mark_as_read(docid, std::move(msg), rename); /* otherwise, mark message and and possible dups as read */ } Server::Server(Store& store, Server::Output output) : priv_{std::make_unique<Private>(store, output)} {} Server::~Server() = default; bool Server::invoke(const std::string& expr) noexcept { return priv_->invoke(expr); } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-server.hh��������������������������������������������������������������������������0000664�0000000�0000000�00000003744�14523230065�0015246�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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_SERVER_HH__ #define MU_SERVER_HH__ #include <memory> #include <functional> #include <utils/mu-sexp.hh> #include <utils/mu-utils.hh> #include <mu-store.hh> namespace Mu { /** * @brief Implements the mu server, as used by mu4e. * */ class Server { public: enum struct OutputFlags { None = 0, SplitList = 1 << 0, /**< insert newlines between list items */ Flush = 1 << 1, /**< flush output buffer after */ }; /** * Prototype for output function * * @param sexp an s-expression * @param flags flags that influence the behavior */ using Output = std::function<void(const Sexp& sexp, OutputFlags flags)>; /** * Construct a new server * * @param store a message store object * @param output callable for the server responses. */ Server(Store& store, Output output); /** * DTOR */ ~Server(); /** * Invoke a call on the server. * * @param expr the s-expression to call * * @return true if we the server is still ready for more * calls, false when it should quit. */ bool invoke(const std::string& expr) noexcept; private: struct Private; std::unique_ptr<Private> priv_; }; MU_ENABLE_BITOPS(Server::OutputFlags); } // namespace Mu #endif /* MU_SERVER_HH__ */ ����������������������������mu-1.10.8/lib/mu-store.cc���������������������������������������������������������������������������0000664�0000000�0000000�00000053516�14523230065�0015064�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2021-2022 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 <chrono> #include <memory> #include <mutex> #include <array> #include <cstdlib> #include <stdexcept> #include <string> #include <unordered_map> #include <atomic> #include <type_traits> #include <iostream> #include <cstring> #include <vector> #include <xapian.h> #include "mu-maildir.hh" #include "mu-store.hh" #include "mu-query.hh" #include "utils/mu-error.hh" #include "utils/mu-utils.hh" #include <utils/mu-utils-file.hh> #include "utils/mu-xapian-utils.hh" using namespace Mu; static_assert(std::is_same<Store::Id, Xapian::docid>::value, "wrong type for Store::Id"); // Properties 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 BatchSizeKey = "batch-size"; constexpr auto DefaultBatchSize = 50'000U; constexpr auto MaxMessageSizeKey = "max-message-size"; constexpr auto DefaultMaxMessageSize = 100'000'000U; constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION; // Stats. constexpr auto ChangedKey = "changed"; constexpr auto IndexedKey = "indexed"; static std::string tstamp_to_string(::time_t t) { char buf[17]; ::snprintf(buf, sizeof(buf), "%" PRIx64, static_cast<int64_t>(t)); return std::string(buf); } static ::time_t string_to_tstamp(const std::string& str) { return static_cast<::time_t>(::strtoll(str.c_str(), {}, 16)); } struct Store::Private { enum struct XapianOpts { ReadOnly, Open, CreateOverwrite }; Private(const std::string& path, bool readonly) : read_only_{readonly}, db_{make_xapian_db(path, read_only_ ? XapianOpts::ReadOnly : XapianOpts::Open)}, properties_{make_properties(path)}, contacts_cache_{db().get_metadata(ContactsKey), properties_.personal_addresses} { } Private(const std::string& path, const std::string& root_maildir, const StringVec& personal_addresses, const Store::Config& conf) : read_only_{false}, db_{make_xapian_db(path, XapianOpts::CreateOverwrite)}, properties_{init_metadata(conf, path, root_maildir, personal_addresses)}, contacts_cache_{"", properties_.personal_addresses} { } ~Private() try { g_debug("closing store @ %s", properties_.database_path.c_str()); if (!read_only_) { transaction_maybe_commit(true /*force*/); } } catch (...) { g_critical("caught exception in store dtor"); } std::unique_ptr<Xapian::Database> make_xapian_db(const std::string db_path, XapianOpts opts) try { /* we do our own flushing, set Xapian's internal one as the * backstop*/ g_setenv("XAPIAN_FLUSH_THRESHOLD", "500000", 1); if (g_mkdir_with_parents(db_path.c_str(), 0700) != 0) throw Mu::Error(Error::Code::Internal, "failed to create database dir %s: %s", db_path.c_str(), ::strerror(errno)); switch (opts) { case XapianOpts::ReadOnly: return std::make_unique<Xapian::Database>(db_path); case XapianOpts::Open: return std::make_unique<Xapian::WritableDatabase>(db_path, Xapian::DB_OPEN); case XapianOpts::CreateOverwrite: return std::make_unique<Xapian::WritableDatabase>( db_path, Xapian::DB_CREATE_OR_OVERWRITE); default: throw std::logic_error("invalid xapian options"); } } catch (const Xapian::DatabaseLockError& xde) { throw Mu::Error(Error::Code::StoreLock, "%s", xde.get_msg().c_str()); } catch (const Xapian::DatabaseError& xde) { throw Mu::Error(Error::Code::Store, "%s", xde.get_msg().c_str()); } catch (const Mu::Error& me) { throw; } catch (...) { throw Mu::Error(Error::Code::Internal, "something went wrong when opening store @ %s", db_path.c_str()); } const Xapian::Database& db() const { return *db_.get(); } Xapian::WritableDatabase& writable_db() { if (read_only_) throw Mu::Error(Error::Code::AccessDenied, "database is read-only"); return dynamic_cast<Xapian::WritableDatabase&>(*db_.get()); } // If not started yet, start a transaction. Otherwise, just update the transaction size. void transaction_inc() noexcept { if (transaction_size_ == 0) { g_debug("starting transaction"); xapian_try([this] { writable_db().begin_transaction(); }); } ++transaction_size_; } // Opportunistically commit a transaction if the transaction size // filled up a batch, or with force. void transaction_maybe_commit(bool force = false) noexcept { if (force || transaction_size_ >= properties_.batch_size) { if (contacts_cache_.dirty()) { xapian_try([&] { writable_db().set_metadata(ContactsKey, contacts_cache_.serialize()); }); } if (indexer_) { // save last index time. if (auto&& t{indexer_->completed()}; t != 0) writable_db().set_metadata( IndexedKey, tstamp_to_string(t)); } if (transaction_size_ == 0) return; // nothing more to do here. g_debug("committing transaction (n=%zu,%zu)", transaction_size_, metadata_cache_.size()); xapian_try([this] { writable_db().commit_transaction(); for (auto&& mdata : metadata_cache_) writable_db().set_metadata(mdata.first, mdata.second); transaction_size_ = 0; }); } } 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()); } Store::Properties make_properties(const std::string& db_path) { Store::Properties props; props.database_path = db_path; props.schema_version = db().get_metadata(SchemaVersionKey); props.created = string_to_tstamp(db().get_metadata(CreatedKey)); props.read_only = read_only_; props.batch_size = ::atoll(db().get_metadata(BatchSizeKey).c_str()); props.max_message_size = ::atoll(db().get_metadata(MaxMessageSizeKey).c_str()); props.root_maildir = db().get_metadata(RootMaildirKey); props.personal_addresses = Mu::split(db().get_metadata(PersonalAddressesKey), ","); return props; } Store::Properties init_metadata(const Store::Config& conf, const std::string& path, const std::string& root_maildir, const StringVec& personal_addresses) { writable_db().set_metadata(SchemaVersionKey, ExpectedSchemaVersion); writable_db().set_metadata(CreatedKey, tstamp_to_string(::time({}))); const size_t batch_size = conf.batch_size ? conf.batch_size : DefaultBatchSize; writable_db().set_metadata(BatchSizeKey, Mu::format("%zu", batch_size)); const size_t max_msg_size = conf.max_message_size ? conf.max_message_size : DefaultMaxMessageSize; writable_db().set_metadata(MaxMessageSizeKey, Mu::format("%zu", max_msg_size)); writable_db().set_metadata(RootMaildirKey, canonicalize_filename(root_maildir, {})); 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); return make_properties(path); } Option<Message> find_message_unlocked(Store::Id docid) const; Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid); Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path); Result<Message> move_message_unlocked(Message&& msg, Option<const std::string&> target_mdir, Option<Flags> new_flags, MoveOptions opts); /* metadata to write as part of a transaction commit */ std::unordered_map<std::string, std::string> metadata_cache_; const bool read_only_{}; std::unique_ptr<Xapian::Database> db_; const Store::Properties properties_; ContactsCache contacts_cache_; std::unique_ptr<Indexer> indexer_; size_t transaction_size_{}; std::mutex lock_; }; Result<Store::Id> Store::Private::update_message_unlocked(Message& msg, Store::Id docid) { return xapian_try_result([&]{ writable_db().replace_document(docid, msg.document().xapian_document()); g_debug("updated message @ %s; docid = %u", msg.path().c_str(), docid); //g_info("%s", msg.sexp().to_string().c_str()); writable_db().set_metadata(ChangedKey, tstamp_to_string(::time({}))); return Ok(std::move(docid)); }); } Result<Store::Id> Store::Private::update_message_unlocked(Message& msg, const std::string& path_to_replace) { return xapian_try_result([&]{ auto id = writable_db().replace_document( field_from_id(Field::Id::Path).xapian_term(path_to_replace), msg.document().xapian_document()); writable_db().set_metadata(ChangedKey, tstamp_to_string(::time({}))); return Ok(std::move(id)); }); } Option<Message> Store::Private::find_message_unlocked(Store::Id docid) const { return xapian_try([&]()->Option<Message> { auto res = Message::make_from_document(db().get_document(docid)); if (res) return Some(std::move(res.value())); else return Nothing; }, Nothing); } Store::Store(const std::string& path, Store::Options opts) : priv_{std::make_unique<Private>(path, none_of(opts & Store::Options::Writable))} { if (none_of(opts & Store::Options::Writable) && any_of(opts & Store::Options::ReInit)) throw Mu::Error(Error::Code::InvalidArgument, "Options::ReInit requires Options::Writable"); if (any_of(opts & Store::Options::ReInit)) { /* user wants to re-initialize an existing store */ Config conf{}; conf.batch_size = properties().batch_size; conf.max_message_size = properties().max_message_size; const auto root_maildir{properties().root_maildir}; const auto addrs{properties().personal_addresses}; /* close the old one */ this->priv_.reset(); /* and create a new one. */ Store new_store(path, root_maildir, addrs, conf); this->priv_ = std::move(new_store.priv_); } /* otherwise, the schema version should match. */ if (properties().schema_version != ExpectedSchemaVersion) throw Mu::Error(Error::Code::SchemaMismatch, "expected schema-version %s, but got %s", ExpectedSchemaVersion, properties().schema_version.c_str()); } Store::Store(const std::string& path, const std::string& maildir, const StringVec& personal_addresses, const Store::Config& conf) : priv_{std::make_unique<Private>(path, maildir, personal_addresses, conf)} { } Store::Store(Store&& other) { priv_ = std::move(other.priv_); priv_->indexer_.reset(); } Store::~Store() = default; const Store::Properties& Store::properties() const { return priv_->properties_; } Store::Statistics Store::statistics() const { Statistics stats{}; stats.size = size(); stats.last_change = string_to_tstamp(priv_->db().get_metadata(ChangedKey)); stats.last_index = string_to_tstamp(priv_->db().get_metadata(IndexedKey)); return stats; } const ContactsCache& Store::contacts_cache() const { return priv_->contacts_cache_; } const Xapian::Database& Store::database() const { return priv_->db(); } Indexer& Store::indexer() { std::lock_guard guard{priv_->lock_}; if (properties().read_only) throw Error{Error::Code::Store, "no indexer for read-only store"}; else if (!priv_->indexer_) priv_->indexer_ = std::make_unique<Indexer>(*this); return *priv_->indexer_.get(); } std::size_t Store::size() const { std::lock_guard guard{priv_->lock_}; return priv_->db().get_doccount(); } bool Store::empty() const { return size() == 0; } Result<Store::Id> Store::add_message(const std::string& path, bool use_transaction) { if (auto msg{Message::make_from_path(path)}; !msg) return Err(msg.error()); else return add_message(msg.value(), use_transaction); } Result<Store::Id> Store::add_message(Message& msg, bool use_transaction) { std::lock_guard guard{priv_->lock_}; const auto mdir{maildir_from_path(msg.path(), properties().root_maildir)}; if (!mdir) return Err(mdir.error()); if (auto&& res = msg.set_maildir(mdir.value()); !res) return Err(res.error()); /* add contacts from this message to cache; this cache * also determines whether those contacts are _personal_, i.e. match * our personal addresses. * * if a message has any personal contacts, mark it as personal; do * this by updating the message flags. */ bool is_personal{}; priv_->contacts_cache_.add(msg.all_contacts(), is_personal); if (is_personal) msg.set_flags(msg.flags() | Flags::Personal); if (use_transaction) priv_->transaction_inc(); auto res = priv_->update_message_unlocked(msg, msg.path()); if (!res) return Err(res.error()); if (use_transaction) /* commit if batch is full */ priv_->transaction_maybe_commit(); g_debug("added %smessage @ %s; docid = %u", is_personal ? "personal " : "", msg.path().c_str(), *res); return res; } Result<Store::Id> Store::update_message(Message& msg, Store::Id docid) { std::lock_guard guard{priv_->lock_}; return priv_->update_message_unlocked(msg, docid); } bool Store::remove_message(const std::string& path) { return xapian_try( [&] { std::lock_guard guard{priv_->lock_}; const auto term{field_from_id(Field::Id::Path).xapian_term(path)}; priv_->writable_db().delete_document(term); priv_->writable_db().set_metadata( ChangedKey, tstamp_to_string(::time({}))); g_debug("deleted message @ %s from store", path.c_str()); return true; }, false); } void Store::remove_messages(const std::vector<Store::Id>& ids) { std::lock_guard guard{priv_->lock_}; priv_->transaction_inc(); xapian_try([&] { for (auto&& id : ids) { priv_->writable_db().delete_document(id); } priv_->writable_db().set_metadata( ChangedKey, tstamp_to_string(::time({}))); }); priv_->transaction_maybe_commit(true /*force*/); } Option<Message> Store::find_message(Store::Id docid) const { std::lock_guard guard{priv_->lock_}; return priv_->find_message_unlocked(docid); } /** * Move a message in store and filesystem. * * Lock is assumed taken already * * @param id message id * @param target_mdir target_midr (or Nothing for current) * @param new_flags new flags (or Notthing) * @param opts move_optionss * * @return the Message after the moving, or an Error */ Result<Message> Store::Private::move_message_unlocked(Message&& msg, Option<const std::string&> target_mdir, Option<Flags> new_flags, MoveOptions opts) { const auto old_path = msg.path(); const auto target_flags = new_flags.value_or(msg.flags()); const auto target_maildir = target_mdir.value_or(msg.maildir()); /* 1. first determine the file system path of the target */ const auto target_path = maildir_determine_target(msg.path(), properties_.root_maildir, target_maildir, target_flags, any_of(opts & MoveOptions::ChangeName)); if (!target_path) return Err(target_path.error()); /* 2. let's move it */ if (const auto res = maildir_move_message(msg.path(), target_path.value()); !res) return Err(res.error()); /* 3. file move worked, now update the message with the new info.*/ if (auto&& res = msg.update_after_move( target_path.value(), target_maildir, target_flags); !res) return Err(res.error()); /* 4. update message worked; re-store it */ if (auto&& res = update_message_unlocked(msg, old_path); !res) return Err(res.error()); /* 6. Profit! */ return Ok(std::move(msg)); } /* get a vec of all messages with the given message id */ static Store::IdMessageVec messages_with_msgid(const Store& store, const std::string& msgid, size_t max=100) { if (msgid.size() > MaxTermLength) { g_warning("invalid message-id '%s'", msgid.c_str()); return {}; } else if (msgid.empty()) return {}; const auto xprefix{field_from_id(Field::Id::MessageId).shortcut}; /*XXX this is a bit dodgy */ auto tmp{g_ascii_strdown(msgid.c_str(), -1)}; auto expr{g_strdup_printf("%c:%s", xprefix, tmp)}; g_free(tmp); const auto res{store.run_query(expr, {}, QueryFlags::None, max)}; g_free(expr); if (!res) { g_warning("failed to run message-id-query: %s", res.error().what()); return {}; } if (res->empty()) { g_warning("could not find message(s) for msgid %s", msgid.c_str()); return {}; } Store::IdMessageVec imvec; for (auto&& mi : *res) imvec.emplace_back(std::make_pair(mi.doc_id(), mi.message().value())); return imvec; } static Flags filter_dup_flags(Flags old_flags, Flags new_flags) { new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Draft); new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Flagged); new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Trashed); return new_flags; } Result<Store::IdMessageVec> Store::move_message(Store::Id id, Option<const std::string&> target_mdir, Option<Flags> new_flags, MoveOptions opts) { std::lock_guard guard{priv_->lock_}; auto msg{priv_->find_message_unlocked(id)}; if (!msg) return Err(Error::Code::Store, "cannot find message <%u>", id); auto res{priv_->move_message_unlocked(std::move(*msg), target_mdir, new_flags, opts)}; if (!res) return Err(res.error()); IdMessageVec imvec; imvec.emplace_back(std::make_pair(id, std::move(*res))); if (none_of(opts & Store::MoveOptions::DupFlags) || !new_flags) return Ok(std::move(imvec)); /* handle the dupflags case; i.e. apply (a subset of) the flags to * all messages with the same message-id as well */ for (auto&& [docid, msg]: messages_with_msgid(*this, imvec.at(0).second.message_id())) { if (docid == id) continue; // already /* For now, don't change Draft/Flagged/Trashed */ Flags dup_flags = filter_dup_flags(msg.flags(), *new_flags); /* use the updated new_flags and default MoveOptions (so we don't recurse, nor do we * change the base-name of moved messages) */ auto dup_res = priv_->move_message_unlocked(std::move(msg), Nothing, dup_flags, Store::MoveOptions::None); // just log a warning if it fails, but continue. if (dup_res) imvec.emplace_back(docid, std::move(*dup_res)); else g_warning("failed to move dup: %s", dup_res.error().what()); } return Ok(std::move(imvec)); } std::string Store::metadata(const std::string& key) const { // get metadata either from the (uncommitted) cache or from the store. std::lock_guard guard{priv_->lock_}; const auto it = priv_->metadata_cache_.find(key); if (it != priv_->metadata_cache_.end()) return it->second; else return xapian_try([&] { return priv_->db().get_metadata(key); }, ""); } void Store::set_metadata(const std::string& key, const std::string& val) { // get metadata either from the (uncommitted) cache or from the store. std::lock_guard guard{priv_->lock_}; priv_->metadata_cache_.erase(key); priv_->metadata_cache_.emplace(key, val); } time_t Store::dirstamp(const std::string& path) const { constexpr auto epoch = static_cast<time_t>(0); const auto ts{metadata(path)}; if (ts.empty()) return epoch; else return static_cast<time_t>(strtoll(ts.c_str(), NULL, 16)); } void Store::set_dirstamp(const std::string& path, time_t tstamp) { std::array<char, 2 * sizeof(tstamp) + 1> data{}; const auto len = static_cast<size_t>( g_snprintf(data.data(), data.size(), "%zx", tstamp)); set_metadata(path, std::string{data.data(), len}); } bool Store::contains_message(const std::string& path) const { return xapian_try( [&] { std::lock_guard guard{priv_->lock_}; const auto term{field_from_id(Field::Id::Path).xapian_term(path)}; return priv_->db().term_exists(term); }, false); } std::size_t Store::for_each_message_path(Store::ForEachMessageFunc msg_func) const { size_t n{}; xapian_try([&] { std::lock_guard guard{priv_->lock_}; Xapian::Enquire enq{priv_->db()}; enq.set_query(Xapian::Query::MatchAll); enq.set_cutoff(0, 0); Xapian::MSet matches(enq.get_mset(0, priv_->db().get_doccount())); constexpr auto path_no{field_from_id(Field::Id::Path).value_no()}; for (auto&& it = matches.begin(); it != matches.end(); ++it, ++n) if (!msg_func(*it, it.get_document().get_value(path_no))) break; }); return n; } void Store::commit() { std::lock_guard guard{priv_->lock_}; priv_->transaction_maybe_commit(true /*force*/); } std::size_t Store::for_each_term(Field::Id field_id, Store::ForEachTermFunc func) const { size_t n{}; xapian_try([&] { /* * Do _not_ take a lock; this is only called from * the message parser which already has the lock */ std::vector<std::string> terms; const auto prefix{field_from_id(field_id).xapian_term()}; for (auto it = priv_->db().allterms_begin(prefix); it != priv_->db().allterms_end(prefix); ++it) { ++n; if (!func(*it)) break; } }); return n; } std::mutex& Store::lock() const { return priv_->lock_; } Result<QueryResults> Store::run_query(const std::string& expr, Field::Id sortfield_id, QueryFlags flags, size_t maxnum) const { return Query{*this}.run(expr, sortfield_id, flags, maxnum); } size_t Store::count_query(const std::string& expr) const { return xapian_try([&] { std::lock_guard guard{priv_->lock_}; Query q{*this}; return q.count(expr); }, 0); } std::string Store::parse_query(const std::string& expr, bool xapian) const { return xapian_try([&] { std::lock_guard guard{priv_->lock_}; Query q{*this}; return q.parse(expr, xapian); }, std::string{}); } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-store.hh���������������������������������������������������������������������������0000664�0000000�0000000�00000031170�14523230065�0015066�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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 <string> #include <vector> #include <mutex> #include <ctime> #include "mu-contacts-cache.hh" #include <xapian.h> #include <utils/mu-utils.hh> #include <index/mu-indexer.hh> #include <mu-query-results.hh> #include <utils/mu-utils.hh> #include <utils/mu-option.hh> #include <message/mu-message.hh> namespace Mu { class Store { public: using Id = Xapian::docid; /**< Id for a message in the store */ static constexpr Id InvalidId = 0; /**< Invalid store id */ /** * Configuration options. */ enum struct Options { None = 0, /**< No specific options */ Writable = 1 << 0, /**< Open in writable mode */ ReInit = 1 << 1, /**< Re-initialize based on existing */ }; /** * Make a store for an existing document database * * @param path path to the database * @param options startup options * * @return A store or an error. */ static Result<Store> make(const std::string& path, Options opts=Options::None) noexcept try { return Ok(Store{path, opts}); } catch (const Mu::Error& me) { return Err(me); } /* LCOV_EXCL_START */ catch (...) { return Err(Error::Code::Internal, "failed to create store"); } /* LCOV_EXCL_STOP */ struct Config { size_t max_message_size{}; /**< maximum size (in bytes) for a message, or 0 for default */ size_t batch_size{}; /**< size of batches before committing, or 0 for default */ }; /** * 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_addresses addresses that should be recognized as * 'personal' for identifying personal messages. * @param config a configuration object * * @return a store or an error */ static Result<Store> make_new(const std::string& path, const std::string& maildir, const StringVec& personal_addresses, const Config& conf) noexcept try { return Ok(Store(path, maildir, personal_addresses, conf)); } catch (const Mu::Error& me) { return Err(me); } /* LCOV_EXCL_START */ catch (...) { return Err(Error::Code::Internal, "failed to create new store"); } /* LCOV_EXCL_STOP */ /** * Move CTOR * */ Store(Store&&); /** * DTOR */ ~Store(); /** * Store properties */ struct Properties { std::string database_path; /**< Full path to the Xapian database */ std::string schema_version; /**< Database schema version */ std::time_t created; /**< database creation time */ bool read_only; /**< Is the database opened read-only? */ size_t batch_size; /**< Maximum database transaction batch size */ bool in_memory; /**< Is this an in-memory database (for testing)?*/ std::string root_maildir; /**< Absolute path to the top-level maildir */ StringVec personal_addresses; /**< Personal e-mail addresses */ size_t max_message_size; /**< Maximus allowed message size */ }; /** * Get properties about this store. * * @return the metadata */ const Properties& properties() const; /** * Store statistics. Unlike the properties, these can change * during the lifetime of a store. * */ struct Statistics { size_t size; /**< number of messages in store */ ::time_t last_change; /**< last time any update happened */ ::time_t last_index; /**< last time an indexing op was performed */ }; /** * Get store statistics * * @return statistics */ Statistics statistics() const; /** * Get the ContactsCache object for this store * * @return the Contacts object */ const ContactsCache& contacts_cache() const; /** * Get the underlying Xapian database for this store. * * @return the database */ const Xapian::Database& database() const; /** * Get the Indexer associated with this store. It is an error to call * this on a read-only store. * * @return the indexer. */ Indexer& indexer(); /** * Run a query; see the `mu-query` man page for the syntax. * * Multi-threaded callers must acquire the lock and keep it * at least as long as the return value. * * @param expr the search expression * @param sortfieldid the sortfield-id. If the field is NONE, sort by DATE * @param flags query flags * @param maxnum maximum number of results to return. 0 for 'no limit' * * @return the query-results or an error. */ std::mutex& lock() const; Result<QueryResults> run_query(const std::string& expr, Field::Id sortfield_id = Field::Id::Date, QueryFlags flags = QueryFlags::None, size_t maxnum = 0) const; /** * run a Xapian query merely to count the number of matches; for the * syntax, please refer to the mu-query manpage * * @param expr the search expression; use "" to match all messages * * @return the number of matches */ size_t count_query(const std::string& expr = "") const; /** * For debugging, get the internal string representation of the parsed * query * * @param expr a xapian search expression * @param xapian if true, show Xapian's internal representation, * otherwise, mu's. * * @return the string representation of the query */ std::string parse_query(const std::string& expr, bool xapian) const; /** * Add a message to the store. When planning to write many messages, * it's much faster to do so in a transaction. If so, set * @in_transaction to true. When done with adding messages, call * commit(). * * @param path the message path. * @param whether to bundle up to batch_size changes in a transaction * * @return the doc id of the added message or an error. */ Result<Id> add_message(const std::string& path, bool use_transaction = false); /** * Add a message to the store. When planning to write many messages, * it's much faster to do so in a transaction. If so, set * @in_transaction to true. When done with adding messages, call * commit(). * * @param msg a message * @param whether to bundle up to batch_size changes in a transaction * * @return the doc id of the added message or an error. */ Result<Id> add_message(Message& msg, bool use_transaction = false); /** * Update a message in the store. * * @param msg a message * @param id the id for this message * * @return Ok() or an error. */ Result<Store::Id> update_message(Message& msg, Id id); /** * Remove a message from the store. It will _not_ remove the message * from the file system. * * @param path the message path. * * @return true if removing happened; false otherwise. */ bool remove_message(const std::string& path); /** * Remove a number if messages from the store. It will _not_ remove the * message from the file system. * * @param ids vector with store ids for the message */ void remove_messages(const std::vector<Id>& ids); /** * Remove a message from the store. It will _not_ remove the message * from the file system. * * @param id the store id for the message */ void remove_message(Id id) { remove_messages({id}); } /** * Find message in the store. * * @param id doc id for the message to find * * @return a message (if found) or Nothing */ Option<Message> find_message(Id id) 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; /** * Options for moving * */ enum struct MoveOptions { None = 0, /**< Defaults */ ChangeName = 1 << 0, /**< Change the name when moving */ DupFlags = 1 << 1, /**< Update flags for duplicate messages too*/ }; /** * Move a message both in the filesystem and in the store. After a * successful move, the message is updated. * * @param id the id for some message * @param target_mdir the target maildir (if any) * @param new_flags new flags (if any) * @param change_name whether to change the name * * @return Result, either a vec of <doc-id, message> for the moved * message(s) or some error. Note that in case of success at least one * message is returned, and only with MoveOptions::DupFlags can it be * more than one. */ using IdMessageVec = std::vector<std::pair<Id, Message>>; Result<IdMessageVec> move_message(Store::Id id, Option<const std::string&> target_mdir = Nothing, Option<Flags> new_flags = Nothing, MoveOptions opts = MoveOptions::None); /** * Prototype for the ForEachMessageFunc * * @param id :t store Id for the message * @param path: the absolute path to the message * * @return true if for_each should continue; false to quit */ using ForEachMessageFunc = std::function<bool(Id, const std::string&)>; /** * Call @param func for each document in the store. This takes a lock on * the store, so the func should _not_ call any other Store:: methods. * * @param func a Callable invoked for each message. * * @return the number of times func was invoked */ size_t for_each_message_path(ForEachMessageFunc func) const; /** * Prototype for the ForEachTermFunc * * @param term: * * @return true if for_each should continue; false to quit */ using ForEachTermFunc = std::function<bool(const std::string&)>; /** * Call @param func for each term for the given field in the store. This * takes a lock on the store, so the func should _not_ call any other * Store:: methods. * * @param id the field id * @param func a Callable invoked for each message. * * @return the number of times func was invoked */ size_t for_each_term(Field::Id id, ForEachTermFunc func) const; /** * Get the store metadata for @p key * * @param key the metadata key * * @return the metadata value or empty for none. */ std::string metadata(const std::string& key) const; /** * Write metadata to the store. * * @param key key * @param val value */ void set_metadata(const std::string& key, const std::string& val); /** * Get the timestamp for some message, or 0 if not found * * @param path the path * * @return the timestamp, or 0 if not found */ time_t message_tstamp(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; /** * Commit the current batch of modifications to disk, opportunistically. * If no transaction is underway, do nothing. */ void commit(); /** * 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: /** * Construct a store for an existing document database * * @param path path to the database * @param options startup options */ Store(const std::string& path, Options opts=Options::None); /** * 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_addresses addresses that should be recognized as * 'personal' for identifying personal messages. * @param config a configuration object */ Store(const std::string& path, const std::string& maildir, const StringVec& personal_addresses, const Config& conf); std::unique_ptr<Private> priv_; }; MU_ENABLE_BITOPS(Store::Options); MU_ENABLE_BITOPS(Store::MoveOptions); } // namespace Mu #endif /* __MU_STORE_HH__ */ ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-tokenizer.cc�����������������������������������������������������������������������0000664�0000000�0000000�00000005352�14523230065�0015735�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-tokenizer.hh�����������������������������������������������������������������������0000664�0000000�0000000�00000006466�14523230065�0015756�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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; case Token::Type::Empty: os << "<empty>"; 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__ */ ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-tree.hh����������������������������������������������������������������������������0000664�0000000�0000000�00000007252�14523230065�0014675�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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 <string_view> #include <iostream> #include <message/mu-fields.hh> #include <utils/mu-option.hh> #include <utils/mu-error.hh> namespace Mu { struct FieldValue { FieldValue(Field::Id idarg, const std::string valarg): field_id{idarg}, val1{valarg} {} FieldValue(Field::Id idarg, const std::string valarg1, const std::string valarg2): field_id{idarg}, val1{valarg1}, val2{valarg2} {} const Field& field() const { return field_from_id(field_id); } const std::string& value() const { return val1; } const std::pair<std::string, std::string> range() const { return { val1, val2 }; } const Field::Id field_id; const std::string val1; const std::string val2; }; /** * operator<< * * @param os an output stream * @param fval a field value. * * @return the updated output stream */ inline std::ostream& operator<<(std::ostream& os, const FieldValue& fval) { os << ' ' << quote(std::string{fval.field().name}); if (fval.field().is_range()) os << ' ' << quote(fval.range().first) << ' ' << quote(fval.range().second); else os << ' ' << quote(fval.value()); return os; } // A node in the parse tree struct Node { enum class Type { Empty, // only for empty trees OpAnd, OpOr, OpXor, OpAndNot, OpNot, Value, ValueAtomic, Range, Invalid }; Node(Type _type, FieldValue&& fval) : type{_type}, field_val{std::move(fval)} {} Node(Type _type) : type{_type} {} Node(Node&& rhs) = default; Type type; Option<FieldValue> field_val; static constexpr std::string_view type_name(Type t) { switch (t) { case Type::Empty: return ""; case Type::OpAnd: return "and"; case Type::OpOr: return "or"; case Type::OpXor: return "xor"; case Type::OpAndNot: return "andnot"; case Type::OpNot: return "not"; case Type::Value: return "value"; case Type::ValueAtomic: return "value_atomic"; case Type::Range: return "range"; case Type::Invalid: return "<invalid>"; default: return "<error>"; } } 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.field_val) os << t.field_val.value(); 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__ */ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-xapian.cc��������������������������������������������������������������������������0000664�0000000�0000000�00000010351�14523230065�0015176�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2017-2022 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 <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) { if (tree.node.type == 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())); } const auto op = std::invoke([](Node::Type ntype) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" switch (ntype) { case Node::Type::OpAnd: return Xapian::Query::OP_AND; case Node::Type::OpOr: return Xapian::Query::OP_OR; case Node::Type::OpXor: return Xapian::Query::OP_XOR; case Node::Type::OpAndNot: return Xapian::Query::OP_AND_NOT; case Node::Type::OpNot: default: throw Mu::Error(Error::Code::Internal, "invalid op"); // bug } #pragma GCC diagnostic pop }, tree.node.type); 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 FieldValue& fval, bool maybe_wildcard) { const auto vlen{fval.value().length()}; if (!maybe_wildcard || vlen <= 1 || fval.value()[vlen - 1] != '*') return Xapian::Query(fval.field().xapian_term(fval.value())); else return Xapian::Query(Xapian::Query::OP_WILDCARD, fval.field().xapian_term(fval.value().substr(0, vlen - 1))); } static Xapian::Query xapian_query_value(const Mu::Tree& tree) { // indexable field implies it can be use with a phrase search. const auto& field_val{tree.node.field_val.value()}; if (!field_val.field().is_indexable_term()) { // /* not an indexable field; no extra magic needed*/ return make_query(field_val, true /*maybe-wildcard*/); } const bool is_atomic = tree.node.type == Node::Type::ValueAtomic; const auto parts{split(field_val.value(), " ")}; if (parts.empty()) return Xapian::Query::MatchNothing; // shouldn't happen else if (parts.size() == 1 && !is_atomic) return make_query(field_val, true /*maybe-wildcard*/); else if (is_atomic) return make_query(field_val, false /*maybe-wildcard*/); std::vector<Xapian::Query> phvec; for (const auto& p : parts) { FieldValue fv{field_val.field_id, p}; phvec.emplace_back(make_query(fv, 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& field_val{tree.node.field_val.value()}; return Xapian::Query(Xapian::Query::OP_VALUE_RANGE, field_val.field().value_no(), field_val.range().first, field_val.range().second); } Xapian::Query Mu::xapian_query(const Mu::Tree& tree) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch-enum" 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: case Node::Type::ValueAtomic: return xapian_query_value(tree); case Node::Type::Range: return xapian_query_range(tree); default: throw Mu::Error(Error::Code::Internal, "invalid query"); // bug } #pragma GCC diagnostic pop } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/mu-xapian.hh��������������������������������������������������������������������������0000664�0000000�0000000�00000002206�14523230065�0015210�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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 MU_XAPIAN_HH__ #define MU_XAPIAN_HH__ #include <xapian.h> #include <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 /* MU_XAPIAN_H__ */ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/��������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0014132�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/bench-indexer.cc����������������������������������������������������������������0000664�0000000�0000000�00000065033�14523230065�0017163�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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 <string> #include <thread> #include <vector> #include <iostream> #include <fstream> #include <utils/mu-utils.hh> #include <utils/mu-regex.hh> #include <mu-store.hh> #include "mu-maildir.hh" #include "utils/mu-test-utils.hh" using namespace Mu; constexpr auto test_msg = R"(Return-Path: <htcondor-users-bounces@cs.wisc.edu> Received: from pop3.web.de [212.227.17.177] by localhost with POP3 (fetchmail-6.4.6) for <arne@localhost> (single-drop); Fri, 26 Jun 2020 12:56:08 +0200 (CEST) Received: from jeeves.cs.wisc.edu ([128.105.6.16]) by mx-ha.web.de (mxweb112 [212.227.17.8]) with ESMTPS (Nemesis) id 1MdMYE-1jFXaM2gnA-00ZKvt for <@ID@@web.de>; Fri, 26 Jun 2020 01:28:11 +0200 Received: from jeeves.cs.wisc.edu (localhost [127.0.0.1]) by jeeves.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLgek013419; Thu, 25 Jun 2020 18:22:23 -0500 Received: from shale.cs.wisc.edu (shale.cs.wisc.edu [128.105.6.25]) by jeeves.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLaf0013414 (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=OK) for <htcondor-users@jeeves.cs.wisc.edu>; Thu, 25 Jun 2020 18:21:36 -0500 Received: from smtp7.wiscmail.wisc.edu (wmmta4.doit.wisc.edu [144.92.197.245]) by shale.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLaMK013694 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES128-GCM-SHA256 bits=128 verify=NO) for <htcondor-users@cs.wisc.edu>; Thu, 25 Jun 2020 18:21:36 -0500 Received: from USG02-CY1-obe.outbound.protection.office365.us ([23.103.209.108]) by smtp7.wiscmail.wisc.edu (Oracle Communications Messaging Server 8.0.2.4.20190812 64bit (built Aug 12 2019)) with ESMTPS id <0QCI042LC8VUXFC0@smtp7.wiscmail.wisc.edu> for htcondor-users@cs.wisc.edu (ORCPT htcondor-users@cs.wisc.edu); Thu, 25 Jun 2020 18:21:31 -0500 (CDT) X-Spam-Report: IsSpam=no, Probability=11%, Hits= RETURN_RECEIPT 0.5, FROM_US_TLD 0.1, HTML_00_01 0.05, HTML_00_10 0.05, SUPERLONG_LINE 0.05, BODYTEXTP_SIZE_3000_LESS 0, BODY_SIZE_10000_PLUS 0, DKIM_SIGNATURE 0, KNOWN_MTA_TFX 0, NO_URI_HTTPS 0, SPF_PASS 0, SXL_IP_TFX_WM 0, WEBMAIL_SOURCE 0, WEBMAIL_XOIP 0, WEBMAIL_X_IP_HDR 0, __ANY_URI 0, __ARCAUTH_DKIM_PASSED 0, __ARCAUTH_DMARC_PASSED 0, __ARCAUTH_PASSED 0, __ATTACHMENT_SIZE_0_10K 0, __ATTACHMENT_SIZE_10_25K 0, __BODY_NO_MAILTO 0, __CT 0, __CTYPE_HAS_BOUNDARY 0, __CTYPE_MULTIPART 0, __HAS_ATTACHMENT 0, __HAS_ATTACHMENT1 0, __HAS_ATTACHMENT2 0, __HAS_FROM 0, __HAS_MSGID 0, __HAS_XOIP 0, __HIGHBITS 0, __MIME_TEXT_P 0, __MIME_TEXT_P1 0, __MIME_TEXT_P2 0, __MIME_VERSION 0, __MULTIPLE_RCPTS_TO_X2 0, __NO_HTML_TAG_RAW 0, __RETURN_RECEIPT_TO 0, __SANE_MSGID 0, __TO_MALFORMED_2 0, __TO_NAME 0, __TO_NAME_DIFF_FROM_ACC 0, __TO_NO_NAME 0, __TO_REAL_NAMES 0, __URI_IN_BODY 0, __URI_MAILTO 0, __URI_NOT_IMG 0, __URI_NO_PATH 0, __URI_NS , __URI_WITHOUT_PATH 0 X-Wisc-Doma: @ID@X@numerica.us,numerica.us X-Wisc-Env-From-B64: d2VzbGV5LnRheWxvckBudW1lcmljYS51cw== X-Spam-PmxInfo: Server=avs-13, Version=6.4.7.2805085, Antispam-Engine: 2.7.2.2107409, Antispam-Data: 2020.6.25.231519, AntiVirus-Engine: 5.74.0, AntiVirus-Data: 2020.6.25.5740002, SenderIP=[23.103.209.108] X-Wisc-DKIM-Verify: @ID@XXXXXXX@numerica.us,numericaus.onmicrosoft.com!pass X-Spam-Score: * ARC-Seal: i=1; a=rsa-sha256; s=arcselector5401; d=microsoft.com; cv=none; b=KyXoddJsnsHsBwhdlO5rcljgMRaylJAUAxWTjG4jQL1C8XJAMgeERtH2sRffdjibYUFfSuDUNJmrTrvrbjKGUt2I8J2M2MgUB/upMoroVPNBrP1Fy9wMeZJQuSS4r4KjZZktsl2i8eq667pzOZO6+wX2IA5M7YtxDqglcWOE6btWzbABVjx+9eCXMt0eMd1+UI6ABK8Frd33EFQLKT0h/cxidWR9l+0gCMAcRxsLrQ82+ckU606AIV/DA1E4Tq7ADe/+CRv4QszDN93pWL/1N2/OOh9vFTs9g9ZG6uXjN+Km/IAdylPbfHgKW60ev3/Bvv6N3pA7DjpuiKj6BnW7mQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector5401; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=OZrj1we1ZUH0xBMhJ5/F6EQnB0cmitFs2xZW1fLMRNs=; b=Pq07a3u26s2UdpucJuVQ0h68272wx46Wp61x/30TelPPFLCRxVjmlH1U3IBmIsZ1jOEtGXFJRv65L3HmwGxRUdLlMOdPRB64BBfHQ9NGWUBykKQmOrJNGJs635nEdpugpzngzIdcg1PS5vHxPJAnOeqoo71OVPI3JqPrPEn2TJJgb9J6PApexkqIbVl35prGPsyS/t2IlYw3/ihWzORG6wvqJeqedgpJTBXeGaDoMa+MQ1BeUsdvybh8+hau4ASpM5lwyeXlGmJ5mUTZi39jp+dFdDrmCj/VM4ezeuXeH9+HFtDjKLZJaTDWUID0IBcr91BaoQE/4r6y+lpkah6LLQ== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass smtp.mailfrom=numerica.us; dmarc=pass action=none header.from=numerica.us; dkim=pass header.d=numerica.us; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=numericaus.onmicrosoft.com; s=selector1-numericaus-onmicrosoft-com; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=OZrj1we1ZUH0xBMhJ5/F6EQnB0cmitFs2xZW1fLMRNs=; b=cFn0eL5k2IKry9U8qa8mbVaxRiyicUAWzRc3NUtj+VEbgShfrz8SO6FPX20WTQQJg/Fu/3isqsSEUt+9NSEEbgd5eQ1EVz5E/JVeNjPe9GXR0JEF/g3f6yM7CO+kKTvXSRvQjce683U0j7Aj1pSDEktoVNP4xvOS2Gx9VjdWTmc= Received: from DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM (2001:489a:200:413::10) by DM3P110MB0490.NAMP110.PROD.OUTLOOK.COM (2001:489a:200:413::14) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3109.25; Thu, 25 Jun 2020 23:21:07 +0000 Received: from DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM ([fe80::f548:f084:9867:9375]) by DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM ([fe80::f548:f084:9867:9375%11]) with mapi id 15.20.3131.024; Thu, 25 Jun 2020 23:21:07 +0000 From: Raul Endymion <XXXXXXXXXXXXXXXXXXXX@numerica.us> To: "'htcondor-users@cs.wisc.edu'" <htcondor-users@cs.wisc.edu> Thread-topic: OPINIONS WANTED: Are there any blatent downsides I am missing to the following Condor configuration Thread-index: AdZLRbEvYoEDBZChS62aOHgPzKD8kw== Date: Thu, 25 Jun 2020 23:21:06 +0000 Message-id: <DM3P110MB04746CDBA55B3E597EFD1877FA920@DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM> Accept-Language: en-US Content-language: en-US X-MS-Has-Attach: yes X-MS-TNEF-Correlator: X-Originating-IP: [50.233.29.54] x-ms-publictraffictype: Email x-ms-office365-filtering-correlation-id: f4edecdf-5582-4b2d-226a-08d8195e7007 x-ms-traffictypediagnostic: DM3P110MB0490: x-microsoft-antispam-prvs: <DM3P110MB04907C313C4243B4FE42A20CFA920@DM3P110MB0490.NAMP110.PROD.OUTLOOK.COM> x-ms-oob-tlc-oobclassifiers: OLM:10000; x-forefront-prvs: 0445A82F82 x-ms-exchange-senderadcheck: 1 x-microsoft-antispam: BCL:0; X-Forefront-Antispam-Report: CIP:255.255.255.255; CTRY:; LANG:en; SCL:1; SRV:; IPV:NLI; SFV:NSPM; H:DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM; PTR:; CAT:NONE; SFTY:; SFS:(346002)(366004)(6916009)(83380400001)(71200400001)(8936002)(55016002)(5660300002)(8676002)(9686003)(66616009)(64756008)(33656002)(52536014)(66446008)(66476007)(66556008)(66946007)(99936003)(76116006)(186003)(86362001)(26005)(7696005)(6506007)(44832011)(508600001)(2906002)(80162005)(80862006)(491001)(554374003); DIR:OUT; SFP:1102; x-ms-exchange-transport-forked: True MIME-version: 1.0 X-OriginatorOrg: numerica.us X-MS-Exchange-CrossTenant-Network-Message-Id: f4edecdf-5582-4b2d-226a-08d8195e7007 X-MS-Exchange-CrossTenant-OriginalArrivalTime: 25 Jun 2020 23:21:06.8341 (UTC) X-MS-Exchange-CrossTenant-fromentityheader: Hosted X-MS-Exchange-CrossTenant-id: fae7a2ae-df1d-444e-91be-babb0900b9c2 X-MS-Exchange-CrossTenant-mailboxtype: HOSTED X-MS-Exchange-Transport-CrossTenantHeadersStamped: DM3P110MB0490 Subject: [HTCondor-users] OPINIONS WANTED: Are there any blatent downsides I am missing to the following Condor configuration X-BeenThere: htcondor-users@cs.wisc.edu X-Mailman-Version: 2.1.19 Precedence: list List-Id: HTCondor-Users Mail List <htcondor-users.cs.wisc.edu> List-Unsubscribe: <https://lists.cs.wisc.edu/mailman/options/htcondor-users>, <mailto:htcondor-users-request@cs.wisc.edu?subject=unsubscribe> List-Archive: <https://www-auth.cs.wisc.edu/lists/htcondor-users/> List-Post: <mailto:htcondor-users@cs.wisc.edu> List-Help: <mailto:htcondor-users-request@cs.wisc.edu?subject=help> List-Subscribe: <https://lists.cs.wisc.edu/mailman/listinfo/htcondor-users>, <mailto:htcondor-users-request@cs.wisc.edu?subject=subscribe> Reply-To: HTCondor-Users Mail List <htcondor-users@cs.wisc.edu> Content-Type: multipart/mixed; boundary="===============0678627779074767862==" Errors-To: htcondor-users-bounces@cs.wisc.edu Sender: "HTCondor-users" <htcondor-users-bounces@cs.wisc.edu> Envelope-To: <@ID@XXXXX@web.de> X-UI-Filterresults: unknown:2;V03:K0:cdojl5YHfkg=:jhTbQXp38SL2za/LB4M7aUwpyw 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 RbReLc8xHJlfLbwdXyGw0HU= --===============0678627779074767862== Content-language: en-US Content-type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg=2.16.840.1.101.3.4.2.3; boundary="----=_NextPart_000_0018_01D64B14.F58791A0" ------=_NextPart_000_0018_01D64B14.F58791A0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Hey! I am architecting our final HTCondor configuration over here and I have = an idea I am unsure about and I would like to ask some experienced users = for their opinion. Background, we have a small, relatively homogenous cluster (with no = special universes) and less than 10 users. Since each user has their own = workstation separate from our cluster I thought the following = configuration would suit our needs, but I want to make sure there isn't = a huge disadvantage I am missing: 1. Set the Central Manager to be highly available to the point of = tolerating N cluster machine failures 2. Put a Submit on each of the users' workstations (I am a little = worried about the resource usage of the condor_shadow and condor_schedd, = my users are already running into RAM consumption issues over time as it = is) 3. Place an Execute on each of the cluster machines, which would lead to = the central manager being on a machine that is also executing jobs Fortunately both my users' and cluster machines all have access to the = same network storage, and we have centralized authentication so we can = just use our users' credentials to authenticate everywhere.=20 Before I set this in dry mud, does anyone have any retrospective = recommendations I could benefit hearing from, since I am still pretty = new to the project? Thank you! -Raul Raul Endymion =E2=80=93 Cluster Manager Numerica Corporation (www.numerica.us) 5042 Technology Parkway #100 Fort Collins, Colorado 80528 =E2=98=8E=EF=B8=8F (970) 207 2233 =F0=9F=93=A7 @ID@XXXXXXXXXXXXXXX@numerica.us ------=_NextPart_000_0018_01D64B14.F58791A0 Content-Type: application/pkcs7-signature; name="smime.p7s" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="smime.p7s" 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 V/zEGk/PEc3pZxNAo7m0I/SXdNulUEkjxai5Wq53i0EhWVLpUU8XY3joXax46yCMqh0PUn90QmMD BybLyFDX6av8tVS5cQs0HbTZdIuj7A/dsKzKKIrSHd3SQ9MLNPRkSRdhagmf5LCF1Y4xEEiuAA/H XdYAxGIcl8n6b2CcLlZzq4W13Ipv8FIZoDsG1u0b9NGfeSOOHidi5kdD6r8lM5PaSPmZsl5PdKK6 +E1Y6rBCvITu0MBo5Tjuwt5cok3Ve0BK5Fg89aIL2/rMicm20qG6nbqxLhHeR0mhPO98KIIzDoeL rLpAlWS7GoPvJqbRzxsCAwEAAaOCAlYwggJSMBAGCSsGAQQBgjcVAQQDAgEBMCMGCSsGAQQBgjcV AgQWBBSv5TU1Bjnw5n3u1iO2y+BHQXk7MTAdBgNVHQ4EFgQUoeMyqBhiyBcgwJN8zbr7pRbgs+sw GQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB Af8wHwYDVR0jBBgwFoAUF+CLMX/eZk96ElRSeiEHqnsujqEwgdMGA1UdHwSByzCByDCBxaCBwqCB v4aBvGxkYXA6Ly8vQ049YWQtR0lMR0FMQUQtQ0EsQ049R2lsZ2FsYWQsQ049Q0RQLENOPVB1Ymxp YyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YWQsREM9 bnVtZXJpY2EsREM9dXM/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNz PWNSTERpc3RyaWJ1dGlvblBvaW50MIHGBggrBgEFBQcBAQSBuTCBtjCBswYIKwYBBQUHMAKGgaZs ZGFwOi8vL0NOPWFkLUdJTEdBTEFELUNBLENOPUFJQSxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNl cyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWFkLERDPW51bWVyaWNhLERDPXVzP2NB Q2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MA0GCSqG SIb3DQEBCwUAA4ICAQBmRoSlPe++k7tsAJOvq0+0dNI6yk6gOBmY4g5jL9NTEjSxPWkeYegIwLr2 UqpiIIZmAh9e9v3z0T2egVyRqNezLPXLkg/2gUfV6D0kRyKtG5mL0yAn/0hkkVyf6jWJpCKmH77x 0w3UpnfKs79jv5YpQDhC2eRFivN50HhIkigLWScPq4zd81ghmN8VFTHVQmsGua/mm1Oj5/pBFuQF B4ljon1N//wX5ZJZaUlJR9eR9tM9m+Gyds2flr5+mZT6Zgm26fKiC5zs91aGnzqGx6s30jfXELP2 FjFrrR46ooV7ehhnyBlCACxIWqXe5sSZsSh9oEYZ7Ux5Vq0thkfArBWsF7HA+LovKCUyHLcXbVBB 6/VAwZ3GLYi/bqbVIEFlVRu4nv/JyKWwoGbAhGyzZNWoeHszFrEIQbQMoMsEumVkMZreE6AxP+zb 6JPPOjlhpymtMo54z1MDYJPyo4HmcpL4xUjHZgqgOxMrbHC4oIVLvKZ/scbVBhPnd0tHHSZqj3ZS gfTvG/ut/tLNTXXe48PkLBw4KguhbLm61Elu3wJALT0UL+ENgUWwb7csUGQBqOyPAHXGYnf/ACOc UBqQckcrK8Jq3u8rnCloW3uDw86hw7MFM+YjmhVRdYRxpJmhKVPT6Amufp2WsSVId8q3CSqTH33L fcxbV1n7hLWHA67MhTCCBq0wggWVoAMCAQICEycAAAsJMaw2RjtHZFUAAQAACwkwDQYJKoZIhvcN AQELBQAwXDESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYKCZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQ BgoJkiaJk/IsZAEZFgJhZDEYMBYGA1UEAxMPYWQtQ0VMRUJSSUFOLUNBMB4XDTIwMDUxMjE1MDk0 MloXDTIxMDcyMjE0MjgwMVowgcExEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkW CG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxETAPBgNVBAsTCE51bWVyaWNhMQ4wDAYDVQQL EwVVc2VyczEYMBYGA1UECxMPUHJlc2VudCBJbnRlcm5zMRYwFAYDVQQDEw1XZXNsZXkgVGF5bG9y MSgwJgYJKoZIhvcNAQkBFhl3ZXNsZXkudGF5bG9yQG51bWVyaWNhLnVzMIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEA5clDLapXkiLVXhAFP9GJv+JJkt+cacyvWaX9xEvqMQXOXb7MqO5E DJE8XPMfxaX84WhuMMePOc9SNUKpDtTa2SHz+AOom+JH38ce2gfrdOPwez/e6RrUb3o8ZvMr3hJl Yy+6vEFEADIICfHSlIjkLJbGNFTRDccvkOPjD2W+fmzFAtWyNb/eqM+mwdTuXjOxTvP6V34zJsvc YKJUzhhD8jI7GdqOoNoirTlaMVTH5udK0P2KvzD6F0LfwcOlc3bTvY9uI585xhdniK4yAIka8OMq 5zmyEQLYOadcVSscjAlkC1sQ0gbwL3AdwS+bntryq+2Ds380OJ+Z1Uy7TRkeBQIDAQABo4IDADCC AvwwPAYJKwYBBAGCNxUHBC8wLQYlKwYBBAGCNxUI9/Bss4wDhbmBGISeqheH4YBfgSWC6qJEgcjE IgIBZQIBKDATBgNVHSUEDDAKBggrBgEFBQcDBDAOBgNVHQ8BAf8EBAMCBaAwGwYJKwYBBAGCNxUK BA4wDDAKBggrBgEFBQcDBDBEBgkqhkiG9w0BCQ8ENzA1MA4GCCqGSIb3DQMCAgIAgDAOBggqhkiG 9w0DBAICAIAwBwYFKw4DAgcwCgYIKoZIhvcNAwcwHQYDVR0OBBYEFDZHoDwoOKD5uzpF/2CcZSeg XWLmMB8GA1UdIwQYMBaAFKHjMqgYYsgXIMCTfM26+6UW4LPrMIHVBgNVHR8Egc0wgcowgceggcSg gcGGgb5sZGFwOi8vL0NOPWFkLUNFTEVCUklBTi1DQSxDTj1DZWxlYnJpYW4sQ049Q0RQLENOPVB1 YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YWQs REM9bnVtZXJpY2EsREM9dXM/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENs YXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIHHBggrBgEFBQcBAQSBujCBtzCBtAYIKwYBBQUHMAKG gadsZGFwOi8vL0NOPWFkLUNFTEVCUklBTi1DQSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2Vy dmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1hZCxEQz1udW1lcmljYSxEQz11 cz9jQUNlcnRpZmljYXRlP2Jhc2U/b2JqZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTBS BgNVHREESzBJoCwGCisGAQQBgjcUAgOgHgwcd2VzbGV5LnRheWxvckBhZC5udW1lcmljYS51c4EZ d2VzbGV5LnRheWxvckBudW1lcmljYS51czANBgkqhkiG9w0BAQsFAAOCAQEAX3zFhiDYU+vQap2J hiysyC9L7nkL7VI2OQWg4Z/JnNJTFiA6BwtoDYAT4qq1Jix4hZc+g78Gj99OnkhlBQDe9Hq12yI9 muboQSDAYO6iDK76wQv3Rt8Fl4SUD4Ygwy52QrkTDrj/HZxTNask5p/2ilGBJnG9KT2VbEgGJkP9 kXn1vAgOl3BCxgjdWekWCvxpmffr+Z3UtmQIiZAB3OsKcgdsSy9pveTMjxtKJemaH3kpXQiTgCev CMuWZb3YnqXI8Fd+uUw6HwA4c+ZH62G9Q8KGkwXyhOPizmm3UeSlMo27yUCE+cF5EIHBxpGJ6z83 7MbxMVKnS1Wz1n8MtW2ezDGCBCEwggQdAgEBMHMwXDESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYK CZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQBgoJkiaJk/IsZAEZFgJhZDEYMBYGA1UEAxMPYWQtQ0VM RUJSSUFOLUNBAhMnAAALCTGsNkY7R2RVAAEAAAsJMA0GCWCGSAFlAwQCAwUAoIICfzAYBgkqhkiG 9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDA2MjUyMzIwNDRaME8GCSqGSIb3 DQEJBDFCBEBaj66vdgjAhEO0p7lO6X44h+LpUlAcROa5Hi4Jp5aWS4hU8CuqOrH12y2GRNmNhKLa 0YieL4fCL3YqDRfop79NMFIGCyqGSIb3DQEJEAIBMUMwQQQdAAAAABAAAACgLzslsB99TKIYKeHy Wh5cAQAAAACAAQAwHTAbgRl3ZXNsZXkudGF5bG9yQG51bWVyaWNhLnVzMIGCBgkrBgEEAYI3EAQx dTBzMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYK CZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNFTEVCUklBTi1DQQITJwAACwkxrDZGO0dkVQAB AAALCTCBhAYLKoZIhvcNAQkQAgsxdaBzMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT 8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNFTEVCUklB Ti1DQQITJwAACwkxrDZGO0dkVQABAAALCTCBkwYJKoZIhvcNAQkPMYGFMIGCMAsGCWCGSAFlAwQB KjALBglghkgBZQMEARYwCgYIKoZIhvcNAwcwCwYJYIZIAWUDBAECMA4GCCqGSIb3DQMCAgIAgDAN BggqhkiG9w0DAgIBQDALBglghkgBZQMEAgMwCwYJYIZIAWUDBAICMAsGCWCGSAFlAwQCATAHBgUr DgMCGjANBgkqhkiG9w0BAQEFAASCAQBNFxhcbK6Rmw0Xyu+79cH5kUsXENcdUaJPKlegcY/gl2BZ 0CPpGcRnwz6z8OPYjvw3jrkiAE8nBbuCKu1CPtuk1h4Cybk7exyMybYvK5xge+N+dz2mFipRfGSY rl/ztX1jyvcDruxaSJwb8WMhAGs505yfaCJfwgFOI3QGi+wUunbOIKy3QQZTXDv89yslZqi0wmeI 8sVRqSAYZRIPEylwS9CU2ReK9BJlfVLZnNP1At4gHE6S2hk8T0eVeLT8uhQiUXXJe4644UoPhoA4 Fxgm7Q62KT6yP9O7c4eZzmQ4A9hdlWM6CtZ5pgMAzLOrVFdypzSc+S1j8DqcFkALCw83AAAAAAAA ------=_NextPart_000_0018_01D64B14.F58791A0-- --===============0678627779074767862== Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline _______________________________________________ HTCondor-users mailing list To unsubscribe, send a message to htcondor-users-request@cs.wisc.edu with a subject: Unsubscribe You can also unsubscribe by visiting https://lists.cs.wisc.edu/mailman/listinfo/htcondor-users The archives can be found at: https://lists.cs.wisc.edu/archive/htcondor-users/ --===============0678627779074767862==--)"; static std::string message(const Regex& rx, size_t id) { char buf[16]; ::snprintf(buf, sizeof(buf), "%zu", id); return to_string_gchar( g_regex_replace(rx, test_msg, -1, 0, buf, G_REGEX_MATCH_DEFAULT, {})); } struct TestData { size_t num_maildirs; size_t num_messages; size_t num_threads; }; static void setup(const TestData& tdata) { /* create toplevel */ auto top_maildir = std::string{BENCH_MAILDIRS}; int res = g_mkdir_with_parents(top_maildir.c_str(), 0700); g_assert_cmpuint(res,==, 0); /* create maildirs */ for (size_t i = 0; i != tdata.num_maildirs; ++i) { const auto mdir = format("%s/maildir-%zu", top_maildir.c_str(), i); auto res = maildir_mkdir(mdir); g_assert(!!res); } const auto rx = Regex::make("@ID@"); /* create messages */ for (size_t n = 0; n != tdata.num_messages; ++n) { auto mpath = format("%s/maildir-%zu/cur/msg-%zu:2,S", top_maildir.c_str(), n % tdata.num_maildirs, n); std::ofstream stream(mpath); auto msg = message(*rx, n); stream.write(msg.c_str(), msg.size()); g_assert_true(stream.good()); } } static void tear_down() { /* ugly */ GError *err{}; const auto cmd{format("/bin/rm -rf '%s' '%s'", BENCH_MAILDIRS, BENCH_STORE)}; if (!g_spawn_command_line_sync(cmd.c_str(), NULL, NULL, NULL, &err)) { g_warning("error: %s\n", err ? err->message : "?"); g_clear_error(&err); } } void black_hole(void) { return; /* do nothing */ } static void benchmark_indexer(gconstpointer testdata) { using namespace std::chrono_literals; using Clock = std::chrono::steady_clock; const auto tdata = reinterpret_cast<const TestData*>(testdata); setup(*tdata); auto start = Clock::now(); { auto store{Store::make_new(BENCH_STORE, BENCH_MAILDIRS, {}, {})}; g_assert_true(!!store); Indexer::Config conf{}; conf.max_threads = tdata->num_threads; auto res = store->indexer().start(conf); g_assert_true(res); while(store->indexer().is_running()) { std::this_thread::sleep_for(100ms); } g_assert_cmpuint(store->size(),==, tdata->num_messages); } const auto elapsed = Clock::now() - start; std::cout << "indexed " << tdata->num_messages << " messages in " << tdata->num_maildirs << " maildirs in " << to_ms(elapsed) << "ms; " << to_us(elapsed) / tdata->num_messages << " μs/message; " << static_cast<size_t>(1000*tdata->num_messages / to_ms(elapsed)) << " messages/s" << " (" << tdata->num_threads << " thread(s))\n"; tear_down(); } int main(int argc, char *argv[]) { size_t num_maildirs{}, num_messages{}; g_test_init(&argc, &argv, nullptr); if (g_test_perf()) { num_maildirs = 20; num_messages = 5000; } else { num_maildirs = 10; num_messages = 1000; } g_log_set_handler( NULL, (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), (GLogFunc)black_hole, NULL); size_t thread_num{}; const auto tnum = g_getenv("THREAD_NUM"); if (tnum) thread_num = ::strtol(tnum, NULL, 10); if (thread_num != 0) { /* THREAD_NUM specified */ static TestData tdata{num_maildirs, num_messages, thread_num}; char *name = g_strdup_printf("/bench/indexer/%zu-cores", thread_num); g_test_add_data_func(name, &tdata, benchmark_indexer); g_free(name); } else { /* no THREAD_NUM specified */ const size_t hw_threads = std::thread::hardware_concurrency(); { static TestData tdata{num_maildirs, num_messages, 1}; g_test_add_data_func("/bench/indexer/1-core", &tdata, benchmark_indexer); } if (hw_threads > 2) { static TestData tdata{num_maildirs, num_messages, hw_threads/2}; char *name = g_strdup_printf("/bench/indexer/%zu-cores", hw_threads/2); g_test_add_data_func(name, &tdata, benchmark_indexer); g_free(name); } if (hw_threads > 1) { static TestData tdata{num_maildirs, num_messages, hw_threads}; char *name = g_strdup_printf("/bench/indexer/%zu-cores", hw_threads); g_test_add_data_func(name, &tdata, benchmark_indexer); g_free(name); } } tear_down(); return g_test_run(); } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/cjk/����������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0014701�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/cjk/cur/������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0015472�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/cjk/cur/test1�������������������������������������������������������������������0000664�0000000�0000000�00000000423�14523230065�0016454�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������From: "Bob" <bob@builder.com> Subject: CJK 1 To: "Chase" <chase@ppatrol.org> Date: Thu, 18 Nov 2021 08:35:34 +0200 Message-Id: 112342343e9dfo.fsf@builder.com User-Agent: mu4e 1.7.5; emacs 29.0.50 サーバがダウンしました https://github.com/djcb/mu/issues/1428 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/cjk/cur/test2�������������������������������������������������������������������0000664�0000000�0000000�00000000422�14523230065�0016454�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������From: "Bob" <bob@builder.com> Subject: CJK 2 To: "Chase" <chase@ppatrol.org> Date: Thu, 18 Nov 2021 08:35:34 +0200 Message-Id: 271r2342343e9dfo.fsf@builder.com User-Agent: mu4e 1.7.5; emacs 29.0.50 スポンサーシップ募集 https://github.com/djcb/mu/issues/1428 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/cjk/cur/test3�������������������������������������������������������������������0000664�0000000�0000000�00000000423�14523230065�0016456�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������From: "Bob" <bob@builder.com> Subject: CJK 3 To: "Chase" <chase@ppatrol.org> Date: Thu, 18 Nov 2021 08:35:34 +0200 Message-Id: 3871r2342343e9dfo.fsf@builder.com User-Agent: mu4e 1.7.5; emacs 29.0.50 サービス開始について https://github.com/djcb/mu/issues/1428 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/cjk/cur/test4�������������������������������������������������������������������0000664�0000000�0000000�00000000415�14523230065�0016460�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������From: "Bob" <bob@builder.com> Subject: CJK 4 To: "Chase" <chase@ppatrol.org> Date: Thu, 18 Nov 2021 08:35:34 +0200 Message-Id: 4871r2342343e9dfo.fsf@builder.com User-Agent: mu4e 1.7.5; emacs 29.0.50 ショルダーバック https://github.com/djcb/mu/issues/1428 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/meson.build���������������������������������������������������������������������0000664�0000000�0000000�00000004765�14523230065�0016310�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Copyright (C) 2021 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. # # tests # test('test-maildir', executable('test-maildir', 'test-mu-maildir.cc', install: false, dependencies: [glib_dep, lib_mu_dep])) test('test-msg', executable('test-msg', 'test-mu-msg.cc', install: false, dependencies: [glib_dep, lib_mu_dep])) test('test-store', executable('test-store', 'test-mu-store.cc', install: false, dependencies: [glib_dep, lib_mu_dep])) test('test-query', executable('test-query', 'test-query.cc', install: false, dependencies: [glib_dep, gmime_dep, lib_mu_dep])) test('test-tokenizer', executable('test-tokenizer', 'test-tokenizer.cc', install: false, dependencies: [glib_dep, lib_mu_dep])) test('test-parser', executable('test-parser', 'test-parser.cc', install: false, dependencies: [glib_dep, gmime_dep, lib_mu_dep])) test('test-store-query', executable('test-store-query', 'test-mu-store-query.cc', install: false, dependencies: [glib_dep, gmime_dep, lib_mu_dep])) # # benchmarks # bench_maildirs=join_paths(meson.current_build_dir(), 'maildirs') bench_store=join_paths(meson.current_build_dir(), 'store') bench_indexer_exe = executable( 'bench-indexer', 'bench-indexer.cc', install:false, cpp_args:['-DBENCH_MAILDIRS="' + bench_maildirs + '"', '-DBENCH_STORE="' + bench_store + '"', ], dependencies: [lib_mu_dep, glib_dep]) benchmark('bench-indexer', bench_indexer_exe, args: ['-m', 'perf']) # # below does _not_ pass; it is believed that it's a false alarm. # https://gitlab.gnome.org/GNOME/glib/-/issues/2662 # also register benchmark as a normal test so it gets included for # valgrind/helgrind etc. # test('test-bench-indexer', bench_indexer_exe, # args : ['-m', 'quick'], env: ['THREADNUM=16']) �����������mu-1.10.8/lib/tests/test-indexer.cc�����������������������������������������������������������������0000664�0000000�0000000�00000003152�14523230065�0017055�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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 <vector> #include <glib.h> #include <iostream> #include <sstream> #include <unistd.h> #include "mu-indexer.hh" #include "utils/mu-utils.hh" #include "test-mu-common.h" using namespace Mu; static void test_index_maildir() { allow_warnings(); Store store{test_mu_common_get_random_tmpdir(), std::string{MU_TESTMAILDIR}}; Indexer idx{Indexer::Config{}, store}; g_assert_true(idx.start()); while (idx.is_running()) { sleep(1); } g_print("again!\n"); g_assert_true(idx.start()); while (idx.is_running()) { sleep(1); } } int main(int argc, char* argv[]) try { g_test_init(&argc, &argv, NULL); g_test_add_func("/indexer/index-maildir", test_index_maildir); return g_test_run(); } catch (const std::runtime_error& re) { std::cerr << re.what() << "\n"; return 1; } catch (...) { std::cerr << "caught exception\n"; return 1; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/test-mu-container.cc������������������������������������������������������������0000664�0000000�0000000�00000004203�14523230065�0020016�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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. ** */ #include "config.h" #include <glib.h> #include "utils/mu-test-utils.hh" #include "mu-container.hh" 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, (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), (GLogFunc)black_hole, NULL); return g_test_run(); } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/test-mu-maildir.cc��������������������������������������������������������������0000664�0000000�0000000�00000035466�14523230065�0017474�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2008-2023 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 <glib/gstdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <vector> #include <fstream> #include "utils/mu-test-utils.hh" #include "mu-maildir.hh" #include "utils/mu-result.hh" using namespace Mu; static void test_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_true(!!maildir_mkdir(mdir, 0755, FALSE)); 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_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_true(!!maildir_mkdir(mdir, 0755, 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_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_true(!!maildir_mkdir(mdir, 0755, FALSE)); 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_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 */ if (geteuid() != 0) g_assert_false(!!maildir_mkdir(mdir, 0755, 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_maildir_mkdir_05(void) { /* this must fail */ g_test_log_set_fatal_handler((GTestLogFatalFunc)ignore_error, NULL); g_assert_false(!!maildir_mkdir({}, 0755, true)); } [[maybe_unused]] static void assert_matches_regexp(const char* str, const char* rx) { if (!g_regex_match_simple(rx, str, (GRegexCompileFlags)0, (GRegexMatchFlags)0)) { if (g_test_verbose()) g_print("%s does not match %s", str, rx); g_assert(0); } } static void test_determine_target_ok(void) { struct TestCase { std::string old_path; std::string root_maildir; std::string target_maildir; Flags new_flags; bool new_name; std::string expected; }; const std::vector<TestCase> testcases = { TestCase{ /* change some flags */ "/home/foo/Maildir/test/cur/123456:2,FR", "/home/foo/Maildir", {}, Flags::Seen | Flags::Passed, false, "/home/foo/Maildir/test/cur/123456:2,PS" }, TestCase{ /* from cur -> new */ "/home/foo/Maildir/test/cur/123456:2,FR", "/home/foo/Maildir", {}, Flags::New, false, "/home/foo/Maildir/test/new/123456" }, TestCase{ /* from new->cur */ "/home/foo/Maildir/test/cur/123456", "/home/foo/Maildir", {}, Flags::Seen | Flags::Flagged, false, "/home/foo/Maildir/test/cur/123456:2,FS" }, TestCase{ /* change maildir */ "/home/foo/Maildir/test/cur/123456:2,FR", "/home/foo/Maildir", "/test2", Flags::Flagged | Flags::Replied, false, "/home/foo/Maildir/test2/cur/123456:2,FR" }, TestCase{ /* remove all flags */ "/home/foo/Maildir/test/new/123456", "/home/foo/Maildir", {}, Flags::None, false, "/home/foo/Maildir/test/cur/123456:2," }, }; for (auto&& testcase: testcases) { const auto res = maildir_determine_target( testcase.old_path, testcase.root_maildir, testcase.target_maildir, testcase.new_flags, testcase.new_name); g_assert_true(!!res); g_assert_cmpstr(testcase.expected.c_str(), ==, res.value().c_str()); } } static void test_determine_target_fail(void) { struct TestCase { std::string old_path; std::string root_maildir; std::string target_maildir; Flags new_flags; bool new_name; std::string expected; }; const std::vector<TestCase> testcases = { TestCase{ /* fail: no absolute path */ "../foo/Maildir/test/cur/123456:2,FR-not-absolute", "/home/foo/Maildir", {}, Flags::Seen | Flags::Passed, false, "/home/foo/Maildir/test/cur/123456:2,PS" }, TestCase{ /* fail: no absolute root */ "/home/foo/Maildir/test/cur/123456:2,FR", "../foo/Maildir-not-absolute", {}, Flags::New, false, "/home/foo/Maildir/test/new/123456" }, TestCase{ /* fail: maildir must start with '/' */ "/home/foo/Maildir/test/cur/123456", "/home/foo/Maildir", "mymaildirwithoutslash", Flags::Seen | Flags::Flagged, false, "/home/foo/Maildir/test/cur/123456:2,FS" }, TestCase{ /* fail: path must be below maildir */ "/home/foo/Maildir/test/cur/123456:2,FR", "/home/bar/Maildir", "/test2", Flags::Flagged | Flags::Replied, false, "/home/foo/Maildir/test2/cur/123456:2,FR" }, TestCase{ /* fail: New cannot be combined */ "/home/foo/Maildir/test/new/123456", "/home/foo/Maildir", {}, Flags::New | Flags::Replied, false, "/home/foo/Maildir/test/cur/123456:2," }, }; for (auto&& testcase: testcases) { const auto res = maildir_determine_target( testcase.old_path, testcase.root_maildir, testcase.target_maildir, testcase.new_flags, testcase.new_name); g_assert_false(!!res); } } static void test_maildir_get_new_path_01(void) { struct { std::string oldpath; Flags flags; std::string newpath; } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", Flags::Replied, "/home/foo/Maildir/test/cur/123456:2,R"}, {"/home/foo/Maildir/test/cur/123456:2,FR", Flags::New, "/home/foo/Maildir/test/new/123456"}, {"/home/foo/Maildir/test/new/123456:2,FR", (Flags::Seen | Flags::Replied), "/home/foo/Maildir/test/cur/123456:2,RS"}, {"/home/foo/Maildir/test/new/1313038887_0.697", (Flags::Seen | Flags::Flagged | Flags::Passed), "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"}, {"/home/foo/Maildir/test/new/1313038887_0.697:2,", (Flags::Seen | Flags::Flagged | Flags::Passed), "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"}, /* note the ':2,' suffix on the new message is * removed */ {"/home/foo/Maildir/trash/new/1312920597.2206_16.cthulhu", Flags::Seen, "/home/foo/Maildir/trash/cur/1312920597.2206_16.cthulhu:2,S"}}; for (int i = 0; i != G_N_ELEMENTS(paths); ++i) { const auto newpath{maildir_determine_target(paths[i].oldpath, "/home/foo/Maildir", {}, paths[i].flags, false)}; assert_valid_result(newpath); assert_equal(*newpath, paths[i].newpath); } } static void test_maildir_get_new_path_02(void) { struct { std::string oldpath; Flags flags; std::string targetdir; std::string newpath; std::string root_maildir; } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", Flags::Replied, "/blabla", "/home/foo/Maildir/blabla/cur/123456:2,R", "/home/foo/Maildir"}, {"/home/bar/Maildir/test/cur/123456:2,FR", Flags::New, "/coffee", "/home/bar/Maildir/coffee/new/123456", "/home/bar/Maildir" }, {"/home/cuux/Maildir/test/new/123456", (Flags::Seen | Flags::Replied), "/tea", "/home/cuux/Maildir/tea/cur/123456:2,RS", "/home/cuux/Maildir"}, {"/home/boy/Maildir/test/new/1313038887_0.697:2,", (Flags::Seen | Flags::Flagged | Flags::Passed), "/stuff", "/home/boy/Maildir/stuff/cur/1313038887_0.697:2,FPS", "/home/boy/Maildir"}}; for (int i = 0; i != G_N_ELEMENTS(paths); ++i) { auto newpath{maildir_determine_target(paths[i].oldpath, paths[i].root_maildir, paths[i].targetdir, paths[i].flags, false)}; assert_valid_result(newpath); assert_equal(*newpath, paths[i].newpath); } } static void test_maildir_get_new_path_custom(void) { struct { std::string oldpath; Flags flags; std::string targetdir; std::string newpath; std::string root_maildir; } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", Flags::Replied, "/blabla", "/home/foo/Maildir/blabla/cur/123456:2,R", "/home/foo/Maildir"}, {"/home/foo/Maildir/test/cur/123456:2,hFeRllo123", Flags::Flagged, "/blabla", "/home/foo/Maildir/blabla/cur/123456:2,F", "/home/foo/Maildir"}, {"/home/foo/Maildir/test/cur/123456:2,abc", Flags::Passed, "/blabla", "/home/foo/Maildir/blabla/cur/123456:2,P", "/home/foo/Maildir"}}; for (int i = 0; i != G_N_ELEMENTS(paths); ++i) { auto newpath{maildir_determine_target(paths[i].oldpath, paths[1].root_maildir, paths[i].targetdir, paths[i].flags, false)}; assert_valid_result(newpath); assert_equal(*newpath, paths[i].newpath); } } static void test_maildir_from_path(void) { unsigned u; struct { std::string path, exp; } cases[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", "/test"}, {"/home/foo/Maildir/lala/new/1313038887_0.697:2,", "/lala"}}; for (u = 0; u != G_N_ELEMENTS(cases); ++u) { auto mdir{maildir_from_path(cases[u].path, "/home/foo/Maildir")}; assert_valid_result(mdir); assert_equal(*mdir, cases[u].exp); } } static void test_maildir_link() { TempDir tmpdir; assert_valid_result(maildir_mkdir(tmpdir.path() + "/foo")); assert_valid_result(maildir_mkdir(tmpdir.path() + "/bar")); const auto srcpath1 = tmpdir.path() + "/foo/cur/msg1"; const auto srcpath2 = tmpdir.path() + "/foo/new/msg2"; { std::ofstream stream(srcpath1); stream.write("cur", 3); g_assert_true(stream.good()); stream.close(); } { std::ofstream stream(srcpath2); stream.write("new", 3); g_assert_true(stream.good()); stream.close(); } assert_valid_result(maildir_link(srcpath1, tmpdir.path() + "/bar", false)); assert_valid_result(maildir_link(srcpath2, tmpdir.path() + "/bar", false)); const auto dstpath1 = tmpdir.path() + "/bar/cur/msg1"; const auto dstpath2 = tmpdir.path() + "/bar/new/msg2"; g_assert_true(g_access(dstpath1.c_str(), F_OK) == 0); g_assert_true(g_access(dstpath2.c_str(), F_OK) == 0); assert_valid_result(maildir_clear_links(tmpdir.path() + "/bar")); g_assert_false(g_access(dstpath1.c_str(), F_OK) == 0); g_assert_false(g_access(dstpath2.c_str(), F_OK) == 0); } static void test_maildir_move(bool use_gio) { TempDir tmpdir; assert_valid_result(maildir_mkdir(tmpdir.path() + "/foo")); assert_valid_result(maildir_mkdir(tmpdir.path() + "/bar")); const auto srcpath1 = tmpdir.path() + "/foo/cur/msg1"; const auto srcpath2 = tmpdir.path() + "/foo/new/msg2"; { std::ofstream stream(srcpath1); stream.write("cur", 3); g_assert_true(stream.good()); stream.close(); } { std::ofstream stream(srcpath2); stream.write("new", 3); g_assert_true(stream.good()); stream.close(); } const auto dstpath = tmpdir.path() + "/test1"; assert_valid_result(maildir_move_message(srcpath1, dstpath, use_gio)); assert_valid_result(maildir_move_message(srcpath2, dstpath, use_gio)); //g_assert_true(g_access(dstpath.c_str(), F_OK) == 0); } static void test_maildir_move_vanilla() { test_maildir_move(false/*!gio*/); } static void test_maildir_move_gio() { test_maildir_move(true/*gio*/); } 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_maildir_mkdir_01); g_test_add_func("/mu-maildir/mu-maildir-mkdir-02", test_maildir_mkdir_02); g_test_add_func("/mu-maildir/mu-maildir-mkdir-03", test_maildir_mkdir_03); g_test_add_func("/mu-maildir/mu-maildir-mkdir-04", test_maildir_mkdir_04); g_test_add_func("/mu-maildir/mu-maildir-mkdir-05", test_maildir_mkdir_05); g_test_add_func("/mu-maildir/mu-maildir-determine-target-ok", test_determine_target_ok); g_test_add_func("/mu-maildir/mu-maildir-determine-target-fail", test_determine_target_fail); // /* get/set flags */ g_test_add_func("/mu-maildir/mu-maildir-get-new-path-01", test_maildir_get_new_path_01); g_test_add_func("/mu-maildir/mu-maildir-get-new-path-02", test_maildir_get_new_path_02); g_test_add_func("/mu-maildir/mu-maildir-get-new-path-custom", test_maildir_get_new_path_custom); g_test_add_func("/mu-maildir/mu-maildir-from-path", test_maildir_from_path); g_test_add_func("/mu-maildir/mu-maildir-link", test_maildir_link); g_test_add_func("/mu-maildir/mu-maildir-move-vanilla", test_maildir_move_vanilla); g_test_add_func("/mu-maildir/mu-maildir-move-gio", test_maildir_move_gio); return g_test_run(); } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/test-mu-msg-fields.cc�����������������������������������������������������������0000664�0000000�0000000�00000006577�14523230065�0020106�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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, 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 "utils/mu-test-utils.hh" #include "mu-message-fields.hh" static void test_mu_msg_field_body(void) { Field::Id field; field = Field::Id::BodyText; 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) { Field::Id field; field = 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) { Field::Id field; field = 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) { Field::Id field; field = Field::Id::Priority; 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) { Field::Id field; field = 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, (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), (GLogFunc)black_hole, NULL); return g_test_run(); } ���������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/test-mu-msg.cc������������������������������������������������������������������0000664�0000000�0000000�00000025526�14523230065�0016635�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2008-2022 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 <stdlib.h> #include <unistd.h> #include <time.h> #include <array> #include <string> #include <locale.h> #include "utils/mu-test-utils.hh" #include "utils/mu-result.hh" #include "utils/mu-utils.hh" #include <message/mu-message.hh> using namespace Mu; using ExpectedContacts = const std::vector<std::pair<std::string, std::string>>; static void assert_contacts_equal(const Contacts& contacts, const ExpectedContacts& expected) { g_assert_cmpuint(contacts.size(), ==, expected.size()); size_t n{}; for (auto&& contact: contacts) { if (g_test_verbose()) g_message("{ \"%s\", \"%s\"},\n", contact.name.c_str(), contact.email.c_str()); assert_equal(contact.name, expected.at(n).first); assert_equal(contact.email, expected.at(n).second); ++n; } g_print("\n"); } static void test_mu_msg_01(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1220863042.12663_1.mindcrime!2,S") .value()}; assert_contacts_equal(msg.to(), {{ "Donald Duck", "gcc-help@gcc.gnu.org" }}); assert_contacts_equal(msg.from(), {{ "Mickey Mouse", "anon@example.com" }}); assert_equal(msg.subject(), "gcc include search order"); assert_equal(msg.message_id(), "3BE9E6535E3029448670913581E7A1A20D852173@" "emss35m06.us.lmco.com"); assert_equal(msg.header("Mailing-List").value_or(""), "contact gcc-help-help@gcc.gnu.org; run by ezmlm"); g_assert_true(msg.priority() == Priority::Normal); g_assert_cmpuint(msg.date(), ==, 1217530645); assert_contacts_equal(msg.all_contacts(), { { "", "gcc-help-owner@gcc.gnu.org"}, { "Mickey Mouse", "anon@example.com" }, { "Donald Duck", "gcc-help@gcc.gnu.org" } }); } static void test_mu_msg_02(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1220863087.12663_19.mindcrime!2,S") .value()}; assert_equal(msg.to().at(0).email, "help-gnu-emacs@gnu.org"); assert_equal(msg.subject(), "Re: Learning LISP; Scheme vs elisp."); assert_equal(msg.from().at(0).email, "anon@example.com"); assert_equal(msg.message_id(), "r6bpm5-6n6.ln1@news.ducksburg.com"); assert_equal(msg.header("Errors-To").value_or(""), "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org"); g_assert_true(msg.priority() /* 'low' */ == Priority::Low); g_assert_cmpuint(msg.date(), ==, 1218051515); g_print("flags: %s\n", Mu::to_string(msg.flags()).c_str()); g_assert_true(msg.flags() == (Flags::Seen|Flags::MailingList)); assert_contacts_equal(msg.all_contacts(), { { "", "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org"}, { "", "anon@example.com"}, { "", "help-gnu-emacs@gnu.org"}, }); } static void test_mu_msg_03(void) { //const GSList* params; auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1283599333.1840_11.cthulhu!2,") .value()}; assert_equal(msg.to().at(0).display_name(), "Bilbo Baggins <bilbo@anotherexample.com>"); assert_equal(msg.subject(), "Greetings from Lothlórien"); assert_equal(msg.from().at(0).display_name(), "Frodo Baggins <frodo@example.com>"); g_assert_true(msg.priority() == Priority::Normal); g_assert_cmpuint(msg.date(), ==, 0); assert_equal(msg.body_text().value_or(""), "\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); // assert_equal((char*)params->data, "charset"); // params = g_slist_next(params); // assert_equal((char*)params->data, "UTF-8"); g_assert_true(msg.flags() == (Flags::Unread)); } static void test_mu_msg_04(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/mail5").value()}; assert_equal(msg.to().at(0).display_name(), "George Custer <gac@example.com>"); assert_equal(msg.subject(), "pics for you"); assert_equal(msg.from().at(0).display_name(), "Sitting Bull <sb@example.com>"); g_assert_true(msg.priority() /* 'low' */ == Priority::Normal); g_assert_cmpuint(msg.date(), ==, 0); g_assert_true(msg.flags() == (Flags::HasAttachment|Flags::Unread)); g_assert_true(msg.flags() == (Flags::HasAttachment|Flags::Unread)); } static void test_mu_msg_multimime(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/multimime!2,FS").value()}; /* ie., are text parts properly concatenated? */ assert_equal(msg.subject(), "multimime"); assert_equal(msg.body_text().value_or(""), "abcdef"); g_assert_true(msg.flags() == (Flags::HasAttachment|Flags::Flagged|Flags::Seen)); } static void test_mu_msg_flags(void) { std::array<std::pair<std::string, Flags>, 2> tests= {{ {MU_TESTMAILDIR4 "/multimime!2,FS", (Flags::Flagged | Flags::Seen | Flags::HasAttachment)}, {MU_TESTMAILDIR4 "/special!2,Sabc", (Flags::Seen)} }}; for (auto&& test: tests) { auto msg = Message::make_from_path(test.first); assert_valid_result(msg); g_assert_true(msg->flags() == test.second); } } static void test_mu_msg_umlaut(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,") .value()}; assert_contacts_equal(msg.to(), { { "Helmut Kröger", "hk@testmu.xxx"}}); assert_contacts_equal(msg.from(), { { "Mü", "testmu@testmu.xx"}}); assert_equal(msg.subject(), "Motörhead"); assert_equal(msg.from().at(0).display_name(), "Mü <testmu@testmu.xx>"); g_assert_true(msg.priority() == Priority::Normal); g_assert_cmpuint(msg.date(), ==, 0); } static void test_mu_msg_references(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,") .value()}; std::array<std::string, 4> expected_refs = { "non-exist-01@msg.id", "non-exist-02@msg.id", "non-exist-03@msg.id", "non-exist-04@msg.id" }; assert_equal_seq_str(msg.references(), expected_refs); assert_equal(msg.thread_id(), expected_refs[0]); } static void test_mu_msg_references_dups(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1252168370_3.14675.cthulhu!2,S") .value()}; std::array<std::string, 6> expected_refs = { "439C1136.90504@euler.org", "4399DD94.5070309@euler.org", "20051209233303.GA13812@gauss.org", "439B41ED.2080402@euler.org", "439A1E03.3090604@euler.org", "20051211184308.GB13513@gauss.org" }; assert_equal_seq_str(msg.references(), expected_refs); assert_equal(msg.thread_id(), expected_refs[0]); } static void test_mu_msg_references_many(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR2 "/bar/cur/181736.eml") .value()}; std::array<std::string, 11> expected_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" }; assert_equal_seq_str(msg.references(), expected_refs); assert_equal(msg.thread_id(), expected_refs[0]); } static void test_mu_msg_tags(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/mail1").value()}; assert_contacts_equal(msg.to(), {{ "Julius Caesar", "jc@example.com" }}); assert_contacts_equal(msg.from(), {{ "John Milton", "jm@example.com" }}); assert_equal(msg.subject(),"Fere libenter homines id quod volunt credunt"); g_assert_true(msg.priority() == Priority::High); g_assert_cmpuint(msg.date(), ==, 1217530645); std::array<std::string, 4> expected_tags = { "Paradise", "losT", "john", "milton" }; assert_equal_seq_str(msg.tags(), expected_tags); } static void test_mu_msg_comp_unix_programmer(void) { auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/181736.eml").value()}; g_assert_true(msg.to().empty()); assert_equal(msg.subject(), "Re: Are writes \"atomic\" to readers of the file?"); assert_equal(msg.from().at(0).display_name(), "Jimbo Foobarcuux <jimbo@slp53.sl.home>"); assert_equal(msg.message_id(), "oktdp.42997$Te.22361@news.usenetserver.com"); auto refs = join(msg.references(), ','); assert_equal(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"); //"jimbo@slp53.sl.home (Jimbo Foobarcuux)"; g_assert_true(msg.priority() == Priority::Normal); g_assert_cmpuint(msg.date(), ==, 1299603860); } static void test_mu_str_prio_01(void) { g_assert_true(priority_name(Priority::Low) == "low"); g_assert_true(priority_name(Priority::Normal) == "normal"); g_assert_true(priority_name(Priority::High) == "high"); } G_GNUC_UNUSED static gboolean ignore_error(const char* log_domain, GLogLevelFlags log_level, const gchar* msg, gpointer user_data) { return FALSE; /* don't abort */ } 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); g_test_add_func("/mu-str/mu-str-prio-01", test_mu_str_prio_01); rv = g_test_run(); return rv; } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/test-mu-store-query.cc����������������������������������������������������������0000664�0000000�0000000�00000052677�14523230065�0020355�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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 "utils/mu-result.hh" #include <array> #include <thread> #include <string> #include <string_view> #include <fstream> #include <unordered_map> #include <mu-store.hh> #include <mu-maildir.hh> #include <utils/mu-utils.hh> #include <utils/mu-test-utils.hh> #include <message/mu-message.hh> using namespace Mu; /// map of some (unique) path-tail to the message-text using TestMap = std::unordered_map<std::string, std::string>; static Store make_test_store(const std::string& test_path, const TestMap& test_map, const StringVec &personal_addresses) { std::string maildir = test_path + "/Maildir"; /* write messages to disk */ for (auto&& item: test_map) { const auto msgpath = maildir + "/" + item.first; /* create the directory for the message */ auto dir = to_string_gchar(g_path_get_dirname(msgpath.c_str())); if (g_test_verbose()) g_message("create message dir %s", dir.c_str()); g_assert_cmpuint(g_mkdir_with_parents(dir.c_str(), 0700), ==, 0); /* write the file */ std::ofstream stream(msgpath); stream.write(item.second.data(), item.second.size()); g_assert_true(stream.good()); stream.close(); } /* make the store */ auto store = Store::make_new(test_path, maildir, personal_addresses, {}); assert_valid_result(store); /* index the messages */ auto res = store->indexer().start({}); g_assert_true(res); while(store->indexer().is_running()) { using namespace std::chrono_literals; std::this_thread::sleep_for(100ms); } if (test_map.size() > 0) g_assert_false(store->empty()); g_assert_cmpuint(store->size(),==,test_map.size()); /* and we have a fully-ready store */ return std::move(store.value()); } static void test_simple() { const TestMap test_msgs = {{ // "sqlite-msg" "Simple mailing list message. { "basic/cur/sqlite-msg:2,S", R"(Return-Path: <sqlite-dev-bounces@sqlite.org> 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) Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> From: "Foo Example" <foo@example.com> To: sqlite-dev@sqlite.org Cc: "Bank of America" <bank@example.com> Bcc: Aku Ankka <donald.duck@duckstad.nl> 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 Precedence: list Reply-To: sqlite-dev@sqlite.org List-Id: <sqlite-dev.sqlite.org> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Sender: sqlite-dev-bounces@sqlite.org 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]; I said: "Aujourd'hui!" )"}, }}; TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; // matches for (auto&& expr: { "Inside", "from:foo@example.com", "from:Foo", "from:\"Foo Example\"", "from:/Foo.*Example/", "recip:\"Bank Of America\"", "cc:bank@example.com", "cc:bank", "cc:america", "bcc:donald.duck@duckstad.nl", "bcc:donald.duck", "bcc:duckstad.nl", "bcc:aku", "bcc:ankka", "bcc:\"aku ankka\"", "date:2008-08-01..2008-09-01", "prio:low", "to:sqlite-dev@sqlite.org", "list:sqlite-dev.sqlite.org", "aujourd'hui", }) { if (g_test_verbose()) g_message("query: '%s'", expr); auto qr = store.run_query(expr); assert_valid_result(qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 1); } auto qr = store.run_query("statement"); assert_valid_result(qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 1); assert_equal(qr->begin().subject().value_or(""), "[sqlite-dev] VM optimization inside sqlite3VdbeExec"); g_assert_true(qr->begin().references().empty()); //g_assert_cmpuint(qr->begin().date().value_or(0), ==, 123454); } static void test_spam_address_components() { const TestMap test_msgs = {{ // "sqlite-msg" "Simple mailing list message. { "spam/cur/spam-msg:2,S", R"(Message-Id: <abcde@foo.bar> From: "Foo Example" <bar@example.com> To: example@example.com Subject: ***SPAM*** this is a test Boo! )"}, }}; TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; g_test_bug("2278"); g_test_bug("2281"); // matches both for (auto&& expr: { "SPAM", "spam", "/.*SPAM.*/", "subject:SPAM", "from:bar@example.com", "subject:\\*\\*\\*SPAM\\*\\*\\*", "bar", "example.com" }) { if (g_test_verbose()) g_message("query: '%s'", expr); auto qr = store.run_query(expr); assert_valid_result(qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 1); } } static void test_dups_related() { const TestMap test_msgs = {{ /* parent */ { "inbox/cur/msg1:2,S", R"(Message-Id: <abcde@foo.bar> From: "Foo Example" <bar@example.com> Date: Sat, 06 Aug 2022 11:01:54 -0700 To: example@example.com Subject: test1 Parent )"}, /* child (dup vv) */ { "boo/cur/msg2:1,S", R"(Message-Id: <edcba@foo.bar> In-Reply-To: <abcde@foo.bar> From: "Foo Example" <bar@example.com> Date: Sat, 06 Aug 2022 13:01:54 -0700 To: example@example.com Subject: Re: test1 Child )"}, /* child (dup ^^) */ { "inbox/cur/msg2:1,S", R"(Message-Id: <edcba@foo.bar> In-Reply-To: <abcde@foo.bar> From: "Foo Example" <bar@example.com> Date: Sat, 06 Aug 2022 14:01:54 -0700 To: example@example.com Subject: Re: test1 Child )"}, }}; TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; { // direct matches auto qr = store.run_query("test1", Field::Id::Date, QueryFlags::None); g_assert_true(!!qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 3); } { // skip duplicate messages; which one is skipped is arbitrary. auto qr = store.run_query("test1", Field::Id::Date, QueryFlags::SkipDuplicates); g_assert_true(!!qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 2); } { // no related auto qr = store.run_query("Parent", Field::Id::Date); g_assert_true(!!qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 1); } { // find related messages auto qr = store.run_query("Parent", Field::Id::Date, QueryFlags::IncludeRelated); g_assert_true(!!qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 3); } { // find related messages, skip dups. the leader message // should _not_ be skipped. auto qr = store.run_query("test1 AND maildir:/inbox", Field::Id::Date, QueryFlags::IncludeRelated| QueryFlags::SkipDuplicates); g_assert_true(!!qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 2); // ie the /boo is to be skipped, since it's not in the leader // set. for (auto&& m: *qr) assert_equal(m.message()->maildir(), "/inbox"); } { // find related messages, find parent from child. auto qr = store.run_query("Child and maildir:/inbox", Field::Id::Date, QueryFlags::IncludeRelated); g_assert_true(!!qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 3); } { // find related messages, find parent from child. // leader message wins auto qr = store.run_query("Child and maildir:/inbox", Field::Id::Date, QueryFlags::IncludeRelated| QueryFlags::SkipDuplicates| QueryFlags::Descending); g_assert_true(!!qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 2); // ie the /boo is to be skipped, since it's not in the leader // set. for (auto&& m: *qr) assert_equal(m.message()->maildir(), "/inbox"); } } static void test_related_missing_root() { const TestMap test_msgs = {{ { "inbox/cur/msg1:2,S", R"(Content-Type: text/plain; charset=utf-8 References: <EZrZOnVCsYfFcX3Ls0VFoRnJdCGV4GM5YtO739l-iOB2ADNH7cIJWb0DaO5Of3BWDUEKq18Rz3a7rNoI96bNwQ==@protonmail.internalid> To: "Joerg Roedel" <joro@8bytes.org>, "Suman Anna" <s-anna@ti.com> Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com> From: "Dan Carpenter" <dan.carpenter@oracle.com> Subject: [PATCH] iommu/omap: fix buffer overflow in debugfs Date: Thu, 4 Aug 2022 17:32:39 +0300 Message-Id: <YuvYh1JbE3v+abd5@kili> List-Id: <kernel-janitors.vger.kernel.org> Precedence: bulk There are two issues here: )"}, { "inbox/cur/msg2:2,S", R"(Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 References: <YuvYh1JbE3v+abd5@kili> <9pEUi_xoxa7NskF7EK_qfrlgjXzGsyw9K7cMfYbo-KI6fnyVMKTpc8E2Fu94V8xedd7cMpn0LlBrr9klBMflpw==@protonmail.internalid> Reply-To: "Laurent Pinchart" <laurent.pinchart@ideasonboard.com> From: "Laurent Pinchart" <laurent.pinchart@ideasonboard.com> Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs List-Id: <kernel-janitors.vger.kernel.org> Message-Id: <YuvzKJM66k+ZPD9c@pendragon.ideasonboard.com> Precedence: bulk In-Reply-To: <YuvYh1JbE3v+abd5@kili> Hi Dan, Thank you for the patch. )"}, { "inbox/cur/msg3:2,S", R"(Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 References: <YuvYh1JbE3v+abd5@kili> <G6TStg8J52Q-uSMTR7wRQdPeloxpZMiEQT_F8_JIDYM25eEPeHGgrNKO0fuO78MiQgD9Mz4BDtsZlZgmPKFe4Q==@protonmail.internalid> To: "Dan Carpenter" <dan.carpenter@oracle.com>, "Joerg Roedel" <joro@8bytes.org>, "Suman Anna" <s-anna@ti.com> Reply-To: "Robin Murphy" <robin.murphy@arm.com> From: "Robin Murphy" <robin.murphy@arm.com> Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs List-Id: <kernel-janitors.vger.kernel.org> Message-Id: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> Precedence: bulk In-Reply-To: <YuvYh1JbE3v+abd5@kili> Date: Thu, 4 Aug 2022 17:31:39 +0100 On 04/08/2022 3:32 pm, Dan Carpenter wrote: > There are two issues here: )"}, { "inbox/new/msg4", R"(Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 References: <YuvYh1JbE3v+abd5@kili> <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> <T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid> To: "Robin Murphy" <robin.murphy@arm.com> Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com> From: "Dan Carpenter" <dan.carpenter@oracle.com> Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs List-Id: <kernel-janitors.vger.kernel.org> Date: Fri, 5 Aug 2022 09:37:02 +0300 In-Reply-To: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com> Precedence: bulk Message-Id: <20220805063702.GH3438@kadam> On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote: > On 04/08/2022 3:32 pm, Dan Carpenter wrote: > > There are two issues here: )"}, }}; TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; { auto qr = store.run_query("fix buffer overflow in debugfs", Field::Id::Date, QueryFlags::IncludeRelated); g_assert_true(!!qr); g_assert_cmpuint(qr->size(), ==, 4); } { auto qr = store.run_query("fix buffer overflow in debugfs and flag:unread", Field::Id::Date, QueryFlags::None); g_assert_true(!!qr); g_assert_cmpuint(qr->size(), ==, 1); assert_equal(qr->begin().message_id().value_or(""), "20220805063702.GH3438@kadam"); assert_equal(qr->begin().thread_id().value_or(""), "YuvYh1JbE3v+abd5@kili"); } { /* this one failed earlier, because the 'protonmail' id is the * first reference, which means it does _not_ have the same * thread-id as the rest; however, we filter these * fake-message-ids now.*/ g_test_bug("2312"); auto qr = store.run_query("fix buffer overflow in debugfs and flag:unread", Field::Id::Date, QueryFlags::IncludeRelated); g_assert_true(!!qr); g_assert_cmpuint(qr->size(), ==, 4); } } static void test_body_matricula() { const TestMap test_msgs = {{ { "basic/cur/matricula-msg:2,S", R"(From: XXX <XX@XX.com> Subject: =?iso-8859-1?Q?EF_-_Pago_matr=EDcula_de_la_matr=EDcula_de_inscripci=F3n_a?= Date: Thu, 4 Aug 2022 14:29:41 +0000 Message-ID: <VE1PR03MB5471882920DE08CFE44D97A0FE9F9@VE1PR03MB5471.eurprd03.prod.outlook.com> Accept-Language: es-AR, es-ES, en-US Content-Language: es-AR X-MS-Has-Attach: yes Content-Type: multipart/mixed; boundary="_004_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_" MIME-Version: 1.0 X-OriginatorOrg: ef.com X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-AuthSource: VE1PR03MB5471.eurprd03.prod.outlook.com --_004_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_ Content-Type: multipart/alternative; boundary="_000_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_" --_000_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_ Content-Type: text/plain; charset="iso-8859-1" Content-Transfer-Encoding: quoted-printable Buenas tardes Familia, Espero que est=E9n muy bien. Ya cargamos en sistema su pre inscripci=F3n para el curso Quedamos atentos ante cualquier consulta que surja. Saludos, )"}, }}; TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; /* i.e., non-utf8 text parts were not converted */ g_test_bug("2333"); // matches for (auto&& expr: { "subject:matrícula", "subject:matricula", "body:atentos", "body:inscripción" }) { if (g_test_verbose()) g_message("query: '%s'", expr); auto qr = store.run_query(expr); assert_valid_result(qr); g_assert_false(qr->empty()); g_assert_cmpuint(qr->size(), ==, 1); } } static void test_duplicate_refresh_real(bool rename) { g_test_bug("2327"); const TestMap test_msgs = {{ "inbox/new/msg", { R"(Message-Id: <abcde@foo.bar> From: "Foo Example" <bar@example.com> Date: Wed, 26 Oct 2022 11:01:54 -0700 To: example@example.com Subject: Rainy night in Helsinki Boo! )"}, }}; /* create maildir with message */ TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; g_debug("%s", store.properties().root_maildir.c_str()); /* ensure we have a proper maildir, with new/, cur/ */ auto mres = maildir_mkdir(store.properties().root_maildir + "/inbox"); assert_valid_result(mres); g_assert_cmpuint(store.size(), ==, 1U); /* * find the one msg with a query */ auto qr = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None); g_assert_true(!!qr); g_assert_cmpuint(qr->size(), ==, 1); const auto old_path = qr->begin().path().value(); const auto old_docid = qr->begin().doc_id(); assert_equal(qr->begin().message()->path(), old_path); g_assert_true(::access(old_path.c_str(), F_OK) == 0); /* * mark as read, i.e. move to cur/; ensure it really moved. */ auto move_opts{rename ? Store::MoveOptions::ChangeName : Store::MoveOptions::None}; auto moved_msgs = store.move_message(old_docid, Nothing, Flags::Seen, move_opts); assert_valid_result(moved_msgs); g_assert_true(moved_msgs->size() == 1); const auto& moved_msg{moved_msgs->at(0).second}; const auto new_path = moved_msg.path(); if (!rename) assert_equal(new_path, store.properties().root_maildir + "/inbox/cur/msg:2,S"); g_assert_cmpuint(store.size(), ==, 1); g_assert_false(::access(old_path.c_str(), F_OK) == 0); g_assert_true(::access(new_path.c_str(), F_OK) == 0); /* also ensure that the cached sexp for the message has been updated; * that's what mu4e uses */ const auto moved_sexp{moved_msg.sexp()}; //std::cerr << "@@ " << *moved_msg << '\n'; g_assert_true(moved_sexp.plistp()); g_assert_true(moved_sexp.has_prop(":path")); assert_equal(moved_sexp.get_prop(":path").string(), new_path); /* * find new message with query, ensure it's really that new one. */ auto qr2 = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None); g_assert_true(!!qr2); g_assert_cmpuint(qr2->size(), ==, 1); assert_equal(qr2->begin().path().value(), new_path); /* index the messages */ auto res = store.indexer().start({}); g_assert_true(res); while(store.indexer().is_running()) { using namespace std::chrono_literals; std::this_thread::sleep_for(100ms); } g_assert_cmpuint(store.size(), ==, 1); /* * ensure query still has the right results */ auto qr3 = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None); g_assert_true(!!qr3); g_assert_cmpuint(qr3->size(), ==, 1); const auto path3{qr3->begin().path().value()}; assert_equal(path3, new_path); assert_equal(qr3->begin().message()->path(), new_path); g_assert_true(::access(path3.c_str(), F_OK) == 0); } static void test_duplicate_refresh() { test_duplicate_refresh_real(false/*no rename*/); } static void test_duplicate_refresh_rename() { test_duplicate_refresh_real(true/*rename*/); } static void test_term_split() { g_test_bug("2365"); // Note the fancy quote in "foo’s bar" const TestMap test_msgs = {{ "inbox/new/msg", { R"(Message-Id: <abcde@foo.bar> From: "Foo Example" <bar@example.com> Date: Wed, 26 Oct 2022 11:01:54 -0700 To: example@example.com Subject: foo’s bar Boo! )"}, }}; TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; /* true: match; false: no match */ const auto cases = std::array<std::pair<const char*, bool>, 6>{{ {"subject:foo's", true}, {"subject:foo*", true}, {"subject:/foo/", true}, {"subject:/foo’s/", true}, /* <-- breaks before PR #2365 */ {"subject:/foo.*bar/", true}, /* <-- breaks before PR #2365 */ {"subject:/foo’s bar/", false}, /* <-- no matching yet */ }}; for (auto&& test: cases) { g_debug("query: %s", test.first); auto qr = store.run_query(test.first); assert_valid_result(qr); if (test.second) g_assert_cmpuint(qr->size(), ==, 1); else g_assert_true(qr->empty()); } } static void test_related_dup_threaded() { // test message sent to self, and copy of received msg. const auto test_msg = R"(From: "Edward Mallory" <ed@leviathan.gb> To: "Laurence Oliphant <oli@hotmail.com> Subject: Boo Date: Wed, 07 Dec 2022 18:38:06 +0200 Message-ID: <875yentbhg.fsf@djcbsoftware.nl> MIME-Version: 1.0 Content-Type: text/plain Boo! )"; const TestMap test_msgs = { {"sent/cur/msg1", test_msg }, {"inbox/cur/msg1", test_msg }, {"inbox/cur/msg2", test_msg }}; TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; g_assert_cmpuint(store.size(), ==, 3); // normal query should give 2 { auto qr = store.run_query("maildir:/inbox", Field::Id::Date, QueryFlags::None); assert_valid_result(qr); g_assert_cmpuint(qr->size(), ==, 2); } // a related query should give 3 { auto qr = store.run_query("maildir:/inbox", Field::Id::Date, QueryFlags::IncludeRelated); assert_valid_result(qr); g_assert_cmpuint(qr->size(), ==, 3); } // a related/threading query should give 3. { auto qr = store.run_query("maildir:/inbox", Field::Id::Date, QueryFlags::IncludeRelated | QueryFlags::Threading); assert_valid_result(qr); g_assert_cmpuint(qr->size(), ==, 3); } } static void test_html() { // test message sent to self, and copy of received msg. const auto test_msg = R"(From: Test <test@example.com> To: abc@example.com Date: Mon, 23 May 2011 10:53:45 +0200 Subject: vla MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d" Message-ID: <10374608.109906.11909.20115aabbccdd.MSGID@mailinglijst.nl> --_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d Content-Type: text/plain; charset="iso-8859-15" Content-Transfer-Encoding: quoted-printable text --_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d Content-Type: text/html; charset="iso-8859-15" Content-Transfer-Encoding: quoted-printable html --_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d-- )"; const TestMap test_msgs = {{"inbox/cur/msg1", test_msg }}; TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, {})}; g_assert_cmpuint(store.size(), ==, 1); { auto qr = store.run_query("body:text", Field::Id::Date, QueryFlags::None); assert_valid_result(qr); g_assert_cmpuint(qr->size(), ==, 1); } { auto qr = store.run_query("body:html", Field::Id::Date, QueryFlags::None); assert_valid_result(qr); g_assert_cmpuint(qr->size(), ==, 1); } } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_test_bug_base("https://github.com/djcb/mu/issues/"); g_test_add_func("/store/query/simple", test_simple); g_test_add_func("/store/query/spam-address-components", test_spam_address_components); g_test_add_func("/store/query/dups-related", test_dups_related); g_test_add_func("/store/query/related-missing-root", test_related_missing_root); g_test_add_func("/store/query/body-matricula", test_body_matricula); g_test_add_func("/store/query/duplicate-refresh", test_duplicate_refresh); g_test_add_func("/store/query/duplicate-refresh-rename", test_duplicate_refresh_rename); g_test_add_func("/store/query/term-split", test_term_split); g_test_add_func("/store/query/related-dup-threaded", test_related_dup_threaded); g_test_add_func("/store/query/html", test_html); return g_test_run(); } �����������������������������������������������������������������mu-1.10.8/lib/tests/test-mu-store.cc����������������������������������������������������������������0000664�0000000�0000000�00000035017�14523230065�0017177�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2008-2022 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 <stdlib.h> #include <thread> #include <array> #include <unistd.h> #include <time.h> #include <fstream> #include <locale.h> #include "utils/mu-test-utils.hh" #include "mu-store.hh" #include "utils/mu-result.hh" #include <utils/mu-utils.hh> #include <utils/mu-utils-file.hh> #include "mu-maildir.hh" using namespace Mu; static std::string MuTestMaildir = Mu::canonicalize_filename(MU_TESTMAILDIR, "/"); static std::string MuTestMaildir2 = Mu::canonicalize_filename(MU_TESTMAILDIR2, "/"); static void test_store_ctor_dtor() { TempDir tempdir; auto store{Store::make_new(tempdir.path(), "/tmp", {}, {})}; assert_valid_result(store); g_assert_true(store->empty()); g_assert_cmpuint(0, ==, store->size()); g_assert_cmpstr(MU_STORE_SCHEMA_VERSION, ==, store->properties().schema_version.c_str()); } static void test_store_reinit() { TempDir tempdir; { Store::Config conf{}; conf.max_message_size = 1234567; conf.batch_size = 7654321; StringVec my_addresses{ "foo@example.com", "bar@example.com" }; auto store{Store::make_new(tempdir.path(), MuTestMaildir, my_addresses, conf)}; assert_valid_result(store); g_assert_true(store->empty()); g_assert_cmpuint(0, ==, store->size()); g_assert_cmpstr(MU_STORE_SCHEMA_VERSION, ==, store->properties().schema_version.c_str()); const auto msgpath{MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"}; const auto id = store->add_message(msgpath); assert_valid_result(id); g_assert_true(store->contains_message(msgpath)); g_assert_cmpuint(store->size(), ==, 1); } //now let's reinitialize it. { auto store{Store::make(tempdir.path(), Store::Options::Writable|Store::Options::ReInit)}; assert_valid_result(store); g_assert_true(store->empty()); assert_equal(store->properties().database_path, tempdir.path()); g_assert_cmpuint(store->properties().batch_size,==,7654321); g_assert_cmpuint(store->properties().max_message_size,==,1234567); const auto addrs{store->properties().personal_addresses}; g_assert_cmpuint(addrs.size(),==,2); g_assert_true(seq_some(addrs, [](auto&& a){return a=="foo@example.com";})); g_assert_true(seq_some(addrs, [](auto&& a){return a=="bar@example.com";})); const auto msgpath{MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"}; const auto id = store->add_message(msgpath); assert_valid_result(id); g_assert_true(store->contains_message(msgpath)); g_assert_cmpuint(store->size(), ==, 1); } } static void test_store_add_count_remove() { TempDir tempdir{false}; auto store{Store::make_new(tempdir.path() + "/xapian", MuTestMaildir, {}, {})}; assert_valid_result(store); const auto msgpath{MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"}; const auto id1 = store->add_message(msgpath); assert_valid_result(id1); store->commit(); g_assert_cmpuint(store->size(), ==, 1); g_assert_true(store->contains_message(msgpath)); const auto id2 = store->add_message(MuTestMaildir2 + "/bar/cur/mail3"); g_assert_false(!!id2); // wrong maildir. store->commit(); const auto msg3path{MuTestMaildir + "/cur/1252168370_3.14675.cthulhu!2,S"}; const auto id3 = store->add_message(msg3path); assert_valid_result(id3); g_assert_cmpuint(store->size(), ==, 2); g_assert_true(store->contains_message(msg3path)); store->remove_message(id1.value()); g_assert_cmpuint(store->size(), ==, 1); g_assert_false( store->contains_message(MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,")); store->remove_message(msg3path); g_assert_true(store->empty()); g_assert_false(store->contains_message(msg3path)); } static void test_message_mailing_list() { constexpr const char *test_message_1 = R"(Return-Path: <sqlite-dev-bounces@sqlite.org> 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) 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: Capybaras United Precedence: list Reply-To: sqlite-dev@sqlite.org List-Id: <sqlite-dev.sqlite.org> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Sender: 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]; )"; TempDir tempdir; auto store{Store::make_new(tempdir.path(), "/home/test/Maildir", {}, {})}; assert_valid_result(store); const auto msgpath{"/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S"}; auto message{Message::make_from_text(test_message_1, msgpath)}; assert_valid_result(message); const auto docid = store->add_message(*message); assert_valid_result(docid); g_assert_cmpuint(store->size(),==, 1); /* ensure 'update' dtrt, i.e., nothing. */ const auto docid2 = store->update_message(*message, *docid); assert_valid_result(docid2); g_assert_cmpuint(store->size(),==, 1); g_assert_cmpuint(*docid,==,*docid2); auto msg2{store->find_message(*docid)}; g_assert_true(!!msg2); assert_equal(message->path(), msg2->path()); g_assert_true(store->contains_message(message->path())); const auto qr = store->run_query("to:sqlite-dev@sqlite.org"); g_assert_true(!!qr); g_assert_cmpuint(qr->size(), ==, 1); } static void test_message_attachments(void) { constexpr const char* msg_text = R"(Return-Path: <foo@example.com> Received: from pop.gmail.com [256.85.129.309] by evergrey with POP3 (fetchmail-6.4.29) for <djcb@localhost> (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET) Sender: "Foo, Example" <foo@example.com> User-agent: mu4e 1.7.11; emacs 29.0.50 From: "Foo Example" <foo@example.com> To: bar@example.com Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?= Date: Thu, 24 Mar 2022 20:04:39 +0200 Organization: ACME Inc. Message-Id: <3144HPOJ0VC77.3H1XTAG2AMTLH@"@WILSONB.COM> MIME-Version: 1.0 X-label: @NextActions operation:mindcrime Queensrÿche Content-Type: multipart/mixed; boundary="=-=-=" --=-=-= Content-Type: text/plain Hello, --=-=-= Content-Type: image/jpeg Content-Disposition: attachment; filename=file-01.bin Content-Transfer-Encoding: base64 AAECAw== --=-=-= Content-Type: audio/ogg Content-Disposition: inline; filename=/tmp/file-02.bin Content-Transfer-Encoding: base64 BAUGBw== --=-=-= Content-Type: message/rfc822 Content-Disposition: attachment; filename="message.eml" From: "Fnorb" <fnorb@example.com> To: Bob <bob@example.com> Subject: news for you Date: Mon, 28 Mar 2022 22:53:26 +0300 Attached message! --=-=-= Content-Type: text/plain World! --=-=-=-- )"; TempDir tempdir; auto store{Store::make_new(tempdir.path(), "/home/test/Maildir", {}, {})}; assert_valid_result(store); auto message{Message::make_from_text( msg_text, "/home/test/Maildir/inbox/cur/1649279256.abcde_1.evergrey:2,S")}; assert_valid_result(message); const auto docid = store->add_message(*message); assert_valid_result(docid); store->commit(); auto msg2{store->find_message(*docid)}; g_assert_true(!!msg2); assert_equal(message->path(), msg2->path()); g_assert_true(store->contains_message(message->path())); // for (auto&& term = msg2->document().xapian_document().termlist_begin(); // term != msg2->document().xapian_document().termlist_end(); ++term) // g_message(">>> %s", (*term).c_str()); const auto stats{store->statistics()}; g_assert_cmpuint(stats.size,==,store->size()); g_assert_cmpuint(stats.last_index,==,0); g_assert_cmpuint(stats.last_change,>=,::time({})); } static void test_index_move() { using namespace std::chrono_literals; const std::string msg_text = R"(From: Valentine Michael Smith <mike@example.com> To: Raul Endymion <raul@example.com> Cc: emacs-devel@gnu.org Subject: Re: multi-eq hash tables Date: Tue, 03 May 2022 20:58:02 +0200 Message-ID: <87h766tzzz.fsf@gnus.org> MIME-Version: 1.0 Content-Type: text/plain Precedence: list List-Id: "Emacs development discussions." <emacs-devel.gnu.org> List-Post: <mailto:emacs-devel@gnu.org> Raul Endymion <raul@example.com> writes: > Maybe we should introduce something like: > > (define-hash-table-test shallow-equal > (lambda (x1 x2) (while (and (consp x1) (consp x2) (eql (car x1) (car x2))) > (setq x1 (cdr x1)) (setq x2 (cdr x2))) > (equal x1 x2))) > ...) Yes, that would be excellent. )"; TempDir tempdir2; { // create a message file. const auto res1 = maildir_mkdir(tempdir2.path() + "/Maildir/a"); assert_valid_result(res1); std::ofstream output{tempdir2.path() + "/Maildir/a/new/msg"}; output.write(msg_text.c_str(), msg_text.size()); output.close(); g_assert_true(output.good()); } // Index it into a store. TempDir tempdir; auto store{Store::make_new(tempdir.path(), tempdir2.path() + "/Maildir", {}, {})}; assert_valid_result(store); store->indexer().start({}); size_t n{}; while (store->indexer().is_running()) { std::this_thread::sleep_for(100ms); g_assert_cmpuint(n++,<=,25); } g_assert_true(!store->indexer().is_running()); const auto& prog{store->indexer().progress()}; g_assert_cmpuint(prog.updated,==,1); g_assert_cmpuint(store->size(), ==, 1); g_assert_false(store->empty()); // Find the message auto qr = store->run_query("path:" + tempdir2.path() + "/Maildir/a/new/msg"); assert_valid_result(qr); g_assert_cmpuint(qr->size(),==,1); const auto msg = qr->begin().message(); g_assert_true(!!msg); // Check the message const auto oldpath{msg->path()}; assert_equal(msg->subject(), "Re: multi-eq hash tables"); g_assert_true(msg->docid() != 0); g_debug("%s", msg->sexp().to_string().c_str()); // Move the message from new->cur std::this_thread::sleep_for(1s); /* ctime should change */ const auto msgs3 = store->move_message(msg->docid(), {}, Flags::Seen); assert_valid_result(msgs3); g_assert_true(msgs3->size() == 1); const auto& msg3{msgs3->at(0).second}; assert_equal(msg3.maildir(), "/a"); assert_equal(msg3.path(), tempdir2.path() + "/Maildir/a/cur/msg:2,S"); g_assert_true(::access(msg3.path().c_str(), R_OK)==0); g_assert_false(::access(oldpath.c_str(), R_OK)==0); g_debug("%s", msg3.sexp().to_string().c_str()); g_assert_cmpuint(store->size(), ==, 1); } static void test_store_move_dups() { const std::string msg_text = R"(From: Valentine Michael Smith <mike@example.com> To: Raul Endymion <raul@example.com> Subject: Re: multi-eq hash tables Date: Tue, 03 May 2022 20:58:02 +0200 Message-ID: <87h766tzzz.fsf@gnus.org> Yes, that would be excellent. )"; TempDir tempdir2; // create a message file + dups const auto res1 = maildir_mkdir(tempdir2.path() + "/Maildir/a"); assert_valid_result(res1); const auto res2 = maildir_mkdir(tempdir2.path() + "/Maildir/b"); assert_valid_result(res2); auto msg1_path = tempdir2.path() + "/Maildir/a/new/msg123"; auto msg2_path = tempdir2.path() + "/Maildir/a/cur/msgabc:2,S"; auto msg3_path = tempdir2.path() + "/Maildir/b/cur/msgdef:2,RS"; TempDir tempdir; auto store{Store::make_new(tempdir.path(), tempdir2.path() + "/Maildir", {}, {})}; assert_valid_result(store); std::vector<Store::Id> ids; for (auto&& p: {msg1_path, msg2_path, msg3_path}) { std::ofstream output{p}; output.write(msg_text.c_str(), msg_text.size()); output.close(); auto res = store->add_message(p); assert_valid_result(res); ids.emplace_back(*res); } g_assert_cmpuint(store->size(), ==, 3); // mark main message (+ dups) as seen auto mres = store->move_message(ids.at(0), {}, Flags::Seen | Flags::Flagged | Flags::Passed, Store::MoveOptions::DupFlags); assert_valid_result(mres); // al three dups should have been updated g_assert_cmpuint(mres->size(), ==, 3); // first should be the original g_assert_cmpuint(mres->at(0).first, ==, ids.at(0)); { // Message 1 const Message& msg = mres->at(0).second; assert_equal(msg.path(), tempdir2.path() + "/Maildir/a/cur/msg123:2,FPS"); g_assert_true(msg.flags() == (Flags::Seen|Flags::Flagged|Flags::Passed)); } // note: Seen and Passed should be added to msg2/3, but Flagged shouldn't // msg3 should loose its R flag. auto check_msg2 = [&](const Message& msg) { assert_equal(msg.path(), tempdir2.path() + "/Maildir/a/cur/msgabc:2,PS"); }; auto check_msg3 = [&](const Message& msg) { assert_equal(msg.path(), tempdir2.path() + "/Maildir/b/cur/msgdef:2,PS"); }; if (mres->at(1).first == ids.at(1)) { check_msg2(mres->at(1).second); check_msg3(mres->at(2).second); } else { check_msg2(mres->at(2).second); check_msg3(mres->at(1).second); } } static void test_store_fail() { { const auto store = Store::make("/root/non-existent-path/12345"); g_assert_false(!!store); } { const auto store = Store::make_new("/../../root/non-existent-path/12345", "/../../root/non-existent-path/54321", {}, {}); g_assert_false(!!store); } } int main(int argc, char* argv[]) { mu_test_init(&argc, &argv); g_test_add_func("/store/ctor-dtor", test_store_ctor_dtor); g_test_add_func("/store/reinit", test_store_reinit); g_test_add_func("/store/add-count-remove", test_store_add_count_remove); g_test_add_func("/store/message/mailing-list", test_message_mailing_list); g_test_add_func("/store/message/attachments", test_message_attachments); g_test_add_func("/store/index/index-move", test_index_move); g_test_add_func("/store/index/move-dups", test_store_move_dups); g_test_add_func("/store/index/fail", test_store_fail); return g_test_run(); } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/test-parser.cc������������������������������������������������������������������0000664�0000000�0000000�00000007142�14523230065�0016716�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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 <vector> #include <glib.h> #include <iostream> #include <sstream> #include "utils/mu-test-utils.hh" #include "mu-parser.hh" #include "utils/mu-result.hh" #include "utils/mu-utils.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) { char* tmpdir = test_mu_common_get_random_tmpdir(); g_assert(tmpdir); auto dummy_store{Store::make_new(tmpdir, "/tmp", {}, {})}; assert_valid_result(dummy_store); g_free(tmpdir); Parser parser{*dummy_store, Parser::Flags::UnitTest}; for (const auto& casus : cases) { WarningVec warnings; const auto tree = parser.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; } assert_equal(casus.expected, ss.str()); } } static void test_basic() { CaseVec cases = { //{ "", R"#((atom :value ""))#"}, { "foo", R"#((value "message-id" "foo"))#", }, {"foo or bar", R"#((or(value "message-id" "foo")(value "message-id" "bar")))#"}, {"foo and bar", R"#((and(value "message-id" "foo")(value "message-id" "bar")))#"}, }; test_cases(cases); } static void test_complex() { CaseVec cases = { {"foo and bar or cuux", R"#((or(and(value "message-id" "foo")(value "message-id" "bar")))#" + std::string(R"#((value "message-id" "cuux")))#")}, {"a and not b", R"#((and(value "message-id" "a")(not(value "message-id" "b"))))#"}, {"a and b and c", R"#((and(value "message-id" "a")(and(value "message-id" "b")(value "message-id" "c"))))#"}, {"(a or b) and c", R"#((and(or(value "message-id" "a")(value "message-id" "b"))(value "message-id" "c")))#"}, {"a b", // implicit and R"#((and(value "message-id" "a")(value "message-id" "b")))#"}, {"a not b", // implicit and not R"#((and(value "message-id" "a")(not(value "message-id" "b"))))#"}, {"not b", // implicit and not R"#((not(value "message-id" "b")))#"}}; test_cases(cases); } G_GNUC_UNUSED 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 "message-id" "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(); } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/test-query.cc�������������������������������������������������������������������0000664�0000000�0000000�00000004625�14523230065�0016572�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** Copyright (C) 2022 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 <vector> #include <glib.h> #include <iostream> #include <sstream> #include <unistd.h> #include "mu-store.hh" #include "mu-query.hh" #include "index/mu-indexer.hh" #include "utils/mu-result.hh" #include "utils/mu-utils.hh" #include "utils/mu-test-utils.hh" using namespace Mu; static void test_query() { allow_warnings(); char* tdir; tdir = test_mu_common_get_random_tmpdir(); auto store = Store::make_new(tdir, std::string{MU_TESTMAILDIR}, {}, {}); assert_valid_result(store); g_free(tdir); auto&& idx{store->indexer()}; g_assert_true(idx.start(Indexer::Config{})); while (idx.is_running()) { sleep(1); } auto dump_matches = [](const QueryResults& res) { size_t n{}; for (auto&& item : res) { std::cout << item.query_match() << '\n'; if (g_test_verbose()) g_debug("%02zu %s %s", ++n, item.path().value_or("<none>").c_str(), item.message_id().value_or("<none>").c_str()); } }; g_assert_cmpuint(store->size(), ==, 19); { const auto res = store->run_query("", {}, QueryFlags::None); g_assert_true(!!res); g_assert_cmpuint(res->size(), ==, 19); dump_matches(*res); g_assert_cmpuint(store->count_query(""), ==, 19); } { const auto res = store->run_query("", Field::Id::Path, QueryFlags::None, 11); g_assert_true(!!res); g_assert_cmpuint(res->size(), ==, 11); dump_matches(*res); } } int main(int argc, char* argv[]) try { mu_test_init(&argc, &argv); g_test_add_func("/query", test_query); return g_test_run(); } catch (const std::runtime_error& re) { std::cerr << re.what() << "\n"; return 1; } catch (...) { std::cerr << "caught exception\n"; return 1; } �����������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/test-tokenizer.cc���������������������������������������������������������������0000664�0000000�0000000�00000010115�14523230065�0017426�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* ** 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 (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(); } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0015610�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/��������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0016401�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/1220863042.12663_1.mindcrime!2,S������������������������������������0000664�0000000�0000000�00000014325�14523230065�0022574�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/1220863060.12663_3.mindcrime!2,S������������������������������������0000664�0000000�0000000�00000027073�14523230065�0022602�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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==-- ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/1220863087.12663_15.mindcrime!2,PS����������������������������������0000664�0000000�0000000�00000014643�14523230065�0023015�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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----- --=-=-=-- ���������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/1220863087.12663_19.mindcrime!2,S�����������������������������������0000664�0000000�0000000�00000007136�14523230065�0022700�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/1220863087.12663_5.mindcrime!2,S������������������������������������0000664�0000000�0000000�00000007165�14523230065�0022615�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/1220863087.12663_7.mindcrime!2,RS�����������������������������������0000664�0000000�0000000�00000012626�14523230065�0022737�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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-- ����������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/1252168370_3.14675.cthulhu!2,S��������������������������������������0000664�0000000�0000000�00000001563�14523230065�0022317�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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. ���������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/1283599333.1840_11.cthulhu!2,���������������������������������������0000664�0000000�0000000�00000000752�14523230065�0022173�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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. ����������������������mu-1.10.8/lib/tests/testdir/cur/1305664394.2171_402.cthulhu!2,��������������������������������������0000664�0000000�0000000�00000001156�14523230065�0022247�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/encrypted!2,S�������������������������������������������������������0000664�0000000�0000000�00000004630�14523230065�0020646�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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----- --=-=-=-- ��������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/multimime!2,FS������������������������������������������������������0000664�0000000�0000000�00000001160�14523230065�0020754�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 --=-=-=-- ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/multirecip!2,S������������������������������������������������������0000664�0000000�0000000�00000000363�14523230065�0021025�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/signed!2,S����������������������������������������������������������0000664�0000000�0000000�00000001644�14523230065�0020124�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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----- --=-=-=-- ��������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/signed-encrypted!2,S������������������������������������������������0000664�0000000�0000000�00000004401�14523230065�0022111�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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----- --=-=-=-- ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/cur/special!2,Sabc������������������������������������������������������0000664�0000000�0000000�00000000536�14523230065�0020740�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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. ������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/new/��������������������������������������������������������������������0000775�0000000�0000000�00000000000�14523230065�0016401�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/new/1220863087.12663_21.mindcrime���������������������������������������0000664�0000000�0000000�00000012701�14523230065�0022341�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 ���������������������������������������������������������������mu-1.10.8/lib/tests/testdir/new/1220863087.12663_23.mindcrime���������������������������������������0000664�0000000�0000000�00000012340�14523230065�0022342�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/new/1220863087.12663_25.mindcrime���������������������������������������0000664�0000000�0000000�00000010173�14523230065�0022346�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 ================================================= �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mu-1.10.8/lib/tests/testdir/new/1220863087.12663_9.mindcrime����������������������������������������0000664�0000000�0000000�00000021747�14523230065�0022301�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������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==-- mu-1.10.8/lib/tests/testdir/tmp/000077500000000000000000000000001452323006500164105ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir/tmp/1220863087.12663.ignore000066400000000000000000000101731452323006500213630ustar00rootroot00000000000000Return-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 ================================================= mu-1.10.8/lib/tests/testdir2/000077500000000000000000000000001452323006500156725ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/Foo/000077500000000000000000000000001452323006500164155ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/Foo/cur/000077500000000000000000000000001452323006500172065ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/Foo/cur/arto.eml000066400000000000000000000561701452323006500206630ustar00rootroot00000000000000Return-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-- mu-1.10.8/lib/tests/testdir2/Foo/cur/fraiche.eml000066400000000000000000000005201452323006500213030ustar00rootroot00000000000000From: 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 mu-1.10.8/lib/tests/testdir2/Foo/cur/mail5000066400000000000000000001323441452323006500201470ustar00rootroot00000000000000From: 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 /9j/4AAQSkZJRgABAQAAAQABAAD/4QvoRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB AAAASAAAABsBCQABAAAASAAAACgBCQABAAAAAgAAADEBAgAOAAAAbgAAADIBAgAUAAAAfAAAABMC CQABAAAAAQAAAGmHBAABAAAAkAAAAN4AAABndGh1bWIgMi4xMS4zADIwMTA6MTA6MTcgMTA6MzM6 MzcABgAAkAcABAAAADAyMjEBkQcABAAAAAECAwAAoAcABAAAADAxMDABoAkAAQAAAAEAAAACoAkA AQAAAMgAAAADoAkAAQAAAGsBAAAAAAAABgADAQMAAQAAAAYAAAAaAQkAAQAAAEgAAAAbAQkAAQAA AEgAAAAoAQkAAQAAAAIAAAABAgQAAQAAACwBAAACAgQAAQAAALMKAAAAAAAA/9j/4AAQSkZJRgAB AQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwc KDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIy MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCACAAEcDASIAAhEBAxEB/8QAHwAA AQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpT VFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5 usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAA AAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEI FEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVm Z2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK 0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDq77xdrX/CQ6laRXjRxQTF ECovA/EUg8Sa6W/5CUuP9xP8K5yWQnxjrw9Lwj9BWjkgZHFAG6mu6yV51OXP+4n/AMTUq61rBB/4 mU2f9xP/AImsJJTuAJFW0YDnfmgCTUPFGqWFq882p3G1eyqmT/47VfRfGGpawkgGp3CyIeg2cj1+ 7XK+O7zybCGMNjzHyR6gD/69ZvgG8zqU67vvRZH4EUAesJe6m/XVLv8ANf8A4mpf7Qvl/wCX+6b6 uP8ACs+ObKdaeh3Hg9aANTw/4gurjxTLpU7tIv2cTKzHpgkH+n5UVheHGI+KWzJwdNP/AKFRQBzD 7f8AhMfEDEHH24j/AMdWrs0oCkDrVKJs+NfEsZ+79u/9kWrd5GqKTmgCstwwkyT0p5uzu61mOzbj zSFn3DmgDB8ePLPe2MEQZyykhRzk9/5Va8D6Vd2Mz3d3CYxJHiPd16+la0hhMybkUvxhj1HWr5uM uB0wMYoA3YJARjvV+DBPasC2lYsOuK3LVunWgCLQRj4sIPXTGP8A4/RSaF/yV2P30tv/AEOigDmY QD408UE9ftw/9AFXpv3iFT9Kgs4t3jXxV6C+H/oAq5cxkMcCgDFltXVyMVVv7iGwtzNcNsQfiT+F a8jbAxdgFAzmuTZZfEV81vG+xTyX/uIPT3P9aAIBr9vdNHcQI/lxk5DDBrfsLuK+jE0MqupPOOx9 KzNY8L6fbaaYrdGXb3BOcnuT+FcpodzN4c8RRRyylrW4IDE9MdM/UUAes2wbOAK27PhRms6CJlwc VrWowRkUAV9CP/F3YffSm/8AQ6Kfo+P+FuWp9dLf/wBDooAxrH/kd/Ff/X8P/Ra1evUOcgVW01Qf G/izIz/py/8Aota2LqPK4xQBxniWc2mi3MxBGFA/Mgf1rmtEF/Z6HNqMNuzvPnY+7G1V6Hoe+T0r qfGmnT3Xhm8WNWJVQ+B/skH+lUPBt3d3PhuzXyBM6xBY0YfKDnALewxmgDE1BfEDaPaXNzMRJPIQ +TjCgDHb69u9ZGt2Us2lrdNDtMLAgq27Kng84Fd74qnaMwWB8qWRTnzUcfePGSOx4ziuf1kzT6S9 tuRHlVUG5sDJOMA+lAHofh5/tvh3T7k4ZnhXcfcDB/UVuRQEdqzvDelPo/hywsJGDSRRjeR0yeTj 2ya3I8/3aAMXSU2/FmzJ/wCgbJ/6FRUunf8AJV7H/sGy/wDoQooAyNJXf448XYPS+X/0Wtb8ynyj 0rm/DIll8W+KDKQ0pvF3FehPlr0rvINMzbfN8rsc7upH0oA5ie3mktZSI1ICn5W43e1ec6ZrDwax facIj9liUNtUcgE8j0IzXrHiqS20rQJbiadoyBsWQjc2T2HvXnvhbREuzeXTbvMlfILcsF6D6jFA GJr+pWE1ymFkwFzhlwo+i1xevazLd3Fva2+UiQhh7kdPyr0jVfA8t0BeXNybe35UK2EJAJwST/QG uS1Pw7HYalbKHUIxYxyDd8wHUnNAHsnhXVBrGhWkrBlmEYVww6sAATXQInA5rn/AOZtIa3mQHZI+ xwfvAnJ6d8n9a6yazEKhlzgUAc1YAr8WbH302X/0IUU6xBPxYsSe2my/+hUUAV/Bdj5fi3xWJJDJ JHeopY8bj5a5OK9AUArwARXEeFjjxh4xbub5f/RYrsIZgJhGTjcuQMGgDnfHiwnw1KJoVkUuB8yg hfeuZ+HemTLpjx3OCZNzKUbPy54/Sut8Z263OlJE1wYgzkkjvgH86yfBb+XYWuIGiEithWzn9aAN loTcO0ctuGjV9oMg5JGCSOOnp9K8/wDH1qH1iERrukRAqqB3Jzj9BXpsk6F+oyCuRjJ54rhNcg+3 Ge5XiUSL5ZGc87sdPagDQ+HlvJHoAdo9h85mUY7dK7WSRCoB6HiuV8IiW10JYs7yszDJ7fN/k1tG Rpb4xj7qpnj3Iwfx5oAwLMgfF+1UHI/suTH/AH3RTLJNnxltx2Olvj/vuigB3hgf8Vp4vH/T8v8A 6AK6aRWFk2CA2CPSua8M4/4T3xcp/wCftD/45XR6q32e1JjUySCRdqA4J3HH9aAKHiJTceH4mliK r5e5lDfMpx2Iqp4eQR6Zp75Y4jX7xyfTn8q29djjbS/LMqxYGFdugNZWlskOh2pKgYj2AqO4OB/M 0AW7+NLQ3Fwi/O6hsk5yRwOO3WuS1qGJtNuvN3iNJkX5e+EIxn8f1re1e4ubq8jSOMiBArZJ/wBY xOcfQcZ+tVNTsYh4dnjmG9PMJIP8XYUAQ20z2Hg6OeJGTYQzd+N3Le+RzXQ6TGwtjLLkuxAy3XAH f8Saw9Mlt7vwsI4yZI9m07xtyM/y5rqodqxIFAIx1oA5iDj4w2ZHfS3/APQjRSw8/GGzx20x/wD0 I0UAee+I/GV/4S+IXiAWlvFKJ7gM28njC+1Ubn4v6xclC1hbAq6vwzdjn+lXviB4X1O88b6lPBYX EkUkgZXWJiDwPQVzH/CH61zjSbwj1EDf4UAbV78YdZvYPJbT7UA+7HP61HbfFXXLW3SFdOtSqZ67 v8fesg+Ddbzn+yL3P/XBv8Kcvg3Xc5Oj3x/7YP8A4UAaY+KuvIQP7PtM5JXKt/jUF78Udcu7F7WS ytEVv4grZHPB61VPg/Ws/wDIGvs9v3T/AOFMPg7XcHOk32P+uD/4UAWLb4l6vb2zQJZ2m1gP4WGC FAz19q17f4va0sSobS04GB8rf41z3/CIayOuk3g/7d2/wqRfCWr8f8S27/78P/hQB33w78Q3fib4 jR3l3HHG6WTxgR5xjOe/1oq78JvCmo6dq8+qXUBhhETQqJAVYsSDkA8496KAP//ZAP/bAEMABQME BAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4k HB4fHv/bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e Hh4eHh4eHh4eHh4eHh4eHv/AABEIAWsAyAMBIgACEQEDEQH/xAAdAAABBAMBAQAAAAAAAAAAAAAH AwQFBgACCAEJ/8QAVhAAAQMCBAIHAwYHCwoGAgMAAQIDEQAEBQYSITFBBwgTIlFhcRSBkRUjMqGx 0RYXQoKSssElJjNDRFJicpOi8CQ0NmNzo7PC0uEnNVNUVYMJRWR08f/EABQBAQAAAAAAAAAAAAAA AAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDqjNudMEyw6lvE3HEqUnUS kDSmeEkkCTB241Aq6X8mgSL5J/PT99DPrX3S232GwohPbp5/6v8A70PMsrSuwYKgkgtjiONB0Ovp lykFQLhJ8+0Fefjoyh/6/wDfFBctsJWkpt0wRzSK9ft7cwVMNmf6IoDOOmjJ5/lB/SFejpmyeT/n I/TFBdFqwACGGR6IFOWbRhZ3Ya349wUBgT0y5QP8qT+n/wBq3T0x5OPG8A/OFCy3tbZCRpt2j+YK cotLMjvWjM/1BQEs9MWTR/LUn0VWfjiyb/72hki1sw5q9kaHL6ApZDFspQHsze3DuCgI6umLJqUg +2Eg+AJ/ZSR6aMmj+UOfon7qozjTACUhlsCP5opo402AQWkeumgIK+mrKAA0uuK8e6R+ykj03ZU5 JePx+6h6gaTGlPHmkUwzDjlrhDKVvtIWXVaUIjj4+6gKKOm7KZUQrtU+YCj/AMteHpuyvvpbeV7l f9NAHEc3Ls8ed9iYZLTZIAI4+vpRAy1i7WKYS3dhtIDghQHIjiKC9npwy3O1rcH3K/6a1V034Ef4 OyfPqlf/AE1WG1KKxAAHhFO0LgTCfhQTf47cGPCwuJ/qr/6a9R004as7YXcn0bX/ANNRLbqhsAOF OWnVgEnh4UEm30wWbhhvBL5e3Jpz/ppwOlMLSC3lzElT4suD/kqHN642YT9tIu3T7h2UYHnQWW36 S9YleXcQA8Qhf7U05b6SMPXiFpYGwfZfu19myl9XZ6leEkR9dVBy7XEFXLcUN+mPEn7VnCrplZQ6 zclaFeCgAQfqoOqbO4L6VBxvsnURrRqConcbisqo9FOYE5mw1zGENdkLllpZQSCUkApO/wCbWUAY 63i9N0x3v5SkHy+aqj5QBXhFqRzbFWrrjrWm5a1ER7Unh/saqOSXP3Bst/4pNBbWhLY1nvCt1NyZ mRWtuoKESCOdKjSOB99BiEjYHYU4ZQpKpTwpArTxpVt8g7RQPmlQkpP2UoFE8VkU0buEkK4A+NbN rCzHDagfQCmN69TtsPjTRLoBA1b0oHFaZPwoH6IPODFJPwOG9asklJJ228a8egp9aBspcHhvQf6T b55eZHWVu6m2kgISDsnaT76Kt46hlpx1WyUJKj7q59xzEFXV9dXC1krccK9/M0CybtYUTrMnj50R uiPGLl69OHBTaWG2isgjdRkffQgFyoqO441eOiW4WM1W8D6aVBXpFAfGCVGQmnDZO07jmaYW74G0 yKfNuJUBB50D0ISAIAiNyK3UUkQIpDtgUhAAIApNToAKQACedAotaZgCfOvQrUCAIpsVAma31kEE bbUG60Dwn1oW9O6S3h2HqnYvK291E1SlEkzvQr6f1qRg1gVHc3B4+lAaeq2rXkFhcz3NPwcXWU06 pzqnchtQ4CkBQjT/AKxVZQDXrpOEXLJO0XYj+xFVDI6lHAbEeLSfsq1dd2UPWyxMG7HHhPZCqtkX /wAhw9Svo9gj7KC4Wx2MCKdt7ncTTaxbGmBz3p4hISNzHhQJrQSYnjXiUkQOYpZzupJJpFK5g7n3 0ChQSkbxSluSFDfam5WACokj1NetPAqA29KCXb0HdQ9TSwQlSduVR6XBpJnetkXWgCTQSTQCRpMk Uk8djzpsm9ExNaOP6pOqBQRWcblNtly/eI3SwoTPiIrnbEXAl7j4UaulrEHLbJ9z2ZEuKS2fQneg Fevy62ASQYoHCHkqXsqrl0U3aWs52mpelLgU3v5jah20/C9zO/hVmyc8Bj+HjfvXCB9dB09bq0mD zqQZUkAVDtriOdO7ZzSYO8UEoHBw3r0KB3mm6XUxERWdpO0UDhJBVvSulKh9kU2bUdzvvSzKu9tI mgXShOjfaTQk6xZ/cOw4GLk7/m0XlqlGxEcCDzoP9Y1KhgNhI/lR/VNAWuqGpJyG3pBHdVPn84qs rTqgqJyKyCI+bXHp2qqygH/XoQEW9i5M6r0CJ/1VVnJKP3Aw8QP4BvYf1RVh69K1lm0BMgXyQPL5 moPJ50YDY+TCP1RQW+yRoaC44HhSygNXe99NbW7lCUKA48K3cdgbk8KDa5IAMcKj3XiklIMTSrj0 zCppi8STwNAo5cEo4RXtq53pn1pke8NIUR5UoydWoA+lBMdrCdjSLr5G8yfCmJdWE6ZpNx2NiZoH 4uZjlThKitJ35TUOl4DntUlZOgoiaAe9Nl+tnDbS1QAULdlzxHgftoMLdm4VzAmKKvTsvsrywUqS y8y42seBBBB9xoSWTa7hVwocGmyrh7qDZpUKFXzoptG77NdoFkwiXD7uFUvCMOu7+5SxbNLccO8A TRd6Nsq3eB5mbVezq9l7ThsCTEUBcQvhtuKdWy0qVJ40wS4NqcNOTvA+NBKpMokRSiFKnx8opo07 KAAYNOGwrko0DlB/JO005Y0gTNNW9pkzTm3SDJJoHIQhIlKZ50IesgFDLdgYgC74/mmjC03KaEXW WBTlmykH/Ox+qqgJnVAV+8S3hMbOA78fnVVlZ1Oio5BbnkHI/tVVlAOOvSIbtlSN8QSD/Y0yyayh 3Ldg4BBNs37+6KedfERaWqo2+UE/8E1FdGdwF5Yw0kgzatmPzRQWdhnRClCTWPAEHeKkENodbSpO xpBy3IJJjegjFwjh6U1eWSoxw50+uGQHPE86j7qEqiY9KBstYSe6B61gfLapB5QaaPOQs77V4twR wNA8TcFyPKsdd25UyS6RBAivXXJSDQOm1jyinlq/CoSY22qDDh1AVI22mUqmNqCpdI+GXOZb9Ng1 I9jZL+qNiVSAPiKiMhZQFixcHGGgkvJPaJJ4JBolsKaTdO9zdTcKV4CdqalLSrtevcQEkeVA7ytl /CcOm7sLVKFugb+VSt06k3WgfSSO9HKss30NsagAkAd0GminCXSr8onegfNKSDO/nJp22tCSDUYF xEJpVDkkwDPrQTTCoGxBmnjDp4KG9QVu4rVxjepS3WY1KVPpQSbSiT508tBvBImo9hJICt96f2yS TwIigkWht6UHes8ojK9mJE+2Dj/UVRhaEpn7aDnWhH71LP8A/uj9VVATupmZyCiePzn/ABVVledT OfxfszG6XYj/AGyqygHfX3A+TrPmTiKZ3/1JqvdFX+h+EqPO2TU91+NrK1Mf/sER/YmoLopSpWR8 II/9sPtNARbNSdAKdzHCtnY0Eq4zypnbFaEAqImnRUFt6p93OgjbyEEn6qhrxQUo+FTGIp7m23rU HeGKBi6E6orRcRtWyiSoxWp4ERQeoAI3NeLSnnW7Q2341s5skcOMUCSEpMk08to0gA/Gmu4HCvW1 qoPMQWtOJIDR/I3HiKyycacfU4o86Y4qpxBcutiUphIpLLy1LGtSN1RsTwoLS0SQpx3uoG4HlSHa gq1JMSa9ullVulJAA5+dN0wYiKCQbcJ+lNLoVzmmjKoEmndudSoVAHpQOrbVq3mIqWtYME7Co9oE xHAeFPWVqCQY250EsyuBsSPWn7C1CI4+tRdq4F7gjzFSduoCBHoKCRYUYAG1CHrQJjKFqqB/nqf1 FUWbck96NO3jNCXrPmMm23Ha9R+oqgJPUvM9H6PLtR/vTWVt1MkoT0eMaQAVpdUYPPtSPurKAb9f ZQNkwnf/AMwR/wAA1GdD7QOQMHUeduPtNSfX3H7n2y4mcSSPT5g1G9D69XR9gxCeFvHHzNBczGkR tWqXIBjlW4HdO0U1XqQsGOe9ApcAKTHEGoe7tyJqWWSBv61roQ6nvCgrD7cEyPqpNCYn0qavrMhW w5U1NsYAAoGLAEmaUcAinItSZ8R4VjjJ0Qo+tBHK+NeoI0kVXs3ZptMIS42wk3FwB9EGAPU0KMWz zma6uCtm67BoHZLSQEx6nc0BdzRfBq0UhKu8BwFI4BchttK1EyrlPCqjhmJOXlrbquHUPOrbBJJ5 8+FaXC32NTguLi2MmCYU2fLjPxoCel0vDUFFQPCnSAQaH+Vc625fRhuKpbtX+CHAruL+6iC0QtKV JUFA8CKByyFE8KeI2CTzpG2E8h8acpQSRCRH20DxlRMGPqp2zOkJB2prZtlRIg/Gn7bSUwNW9A+t BAO4B8Kk7UeJmo20SUkmeHlUnbK2G1A+aiZjfxoS9Z8k5Ltwrleo4f1VUV0d6IG/OhR1mkg5HZkz /lqP1VUBI6l5H4vmRJOzvHl86aykOpOf3iEb/Sdj+0rKCgdfUj5NtxzGJI9/zBqP6Gkj8XuDDn2H /Malev6lHybakQD8oNg7f6hVMehVIV0dYMqJhj/mNBcm29Q24U3u2gDtUohvS2dtzTO6H+IoI93d PmBTcq0xBmnDoM7b7U1KZUQCBvQLKcQtuFRSCwAmBFIvBxJ2MGaVAUUhQIngaBNUg7kVV81YoUhV rbuweCiOPpUxmC9Rh9it5Su+dkDmTQ9Nz2q1PrO5JCd5JNAxxyztBaOOXyktsNp1rUefqaH7dpc5 hvlDCLJabVJgOLG5H2D0q7YhhV5nDFrfBLYrTbA9reOAwAkHYep/YKL+X8qWGHYe1aWjCUBtMDQn c+cmgDtvlq7scEQ2grTdtnVqKdhVXx3EscYTpumoIkdogbKHmDXRWJ2NotlxpbjRUOYVJ9N6G+bc Mt3EdghaFx9IAUAXfxB25bIe0haOHd+qiR0QZ1dRdIwXFXdbayEsOqP0T/NNU/MuXH7Qret0KKUn dIE/CoCxUW3UnfcyCOINB2BaiRI8Nop+0gxP21R+iPHF45gCUPq1XVsezd347bGr8wlQERQLWp0r Gw41IoAKp2k0xQkg/Qnwp9bSYJSZFA7YSfjUhbN6uMAimTBIUCakbZW44UD1hkJAoUdZ9pKMgNqH H25H6qqLTR7oPOhZ1ngD0dSOIvGz9SqC4dSZJGQyeRU7H6YrK36lBH4vk7flvQfzxWUFE6/oBwm1 Vz+UWh7+wVTboPSR0cYKkCf8nk/pGpHr/o/e9aLjf5SbH+4XTPoOUk9G2CyCT7PwA/pGgvaR3eAN M71E8qkkJBRsDvTa7QNBFBCuoHGKaqbGuZEU/udIERvTbUmSNO9A0ukjSTG9Nm1BC5PDnUg8pIHC ozEn0tMLcCeCSfqoB9nrElXOLi2bJ0IOkeE86rVzcBDKnAoBLRgCNyfL/HjXmO4koPaoPaOEmf5s 86aYHbPY7mmywdk6mmyHbjwKZkz60Bl6NcGRhWX0XDrWm6vPnnIHeAPAe4VZx2iEFSVrKYgpI0/X UJeYuzhrae0Qt9SANmwdIH7ajh0j4WrW204dYEEE8NqCXW6+7dP2yUMNnTKdZJ24cao2ZGCVO9ol tzcmUnh/j7qXTm3tbC8xdcKQnsWRJ7pIUoq+qKqeMZ3YUp9xxxsJcUCQngOJ2+NAmptDxKHEqSCN vGhnnCxOHY8sISEpe76YECecfb76J2GXDV8wLj2a4DDhlDikEA+h8Kr/AEk4St7Chdtd4sHUI4xz Hw391At0JY38mZntmnHdLV38y4nlP5J/x4102ywVAEbiuLMDfcYeauGjpUlQUDHMGu1MpXPyll+x vkbh5hKifOKBZtuCRoiKWbQoGYpx2KkmYO/lSiGjI2EeYoMaQSSDTplJA2rxtJG1OmUgD/tQKM6h 50MOsv3ujlwgzF02ftopJMAAxt5ULeslKujq5I/9w2frNBb+pM6Dkfsuep8n9NH31lJ9SUfvOBgb F/cf10VlBVuv7q/B61g7fKTW3/0Lpn0IjT0d4IN/82HPzNO+v8Vfg/apkR8osn/crpr0HEno5wUE cLeJnzNARGSCmCD8ab3IBkAcqctjbhwrV1oqTMAUEBdpABnemRAHvqYu7WASQDNMOw7xAKfHjQMn YjvDaq/m64RaYFevcNLSj9VWp+3IEyKo3S8U2+RcSXIkthI9SQKAKXV4u5ue21BLY3JngImrb0S2 TrmCXuYVuKb9pfUkr0nZpA4Ty3maHDSXsQLOHW/8PdOJZbHLvH7AAfjXUOW8rWWGZbssMTZtutMt BGpbQMniVRE7mTQDzMfSJctYXcpwbLq30MoSFP3KinXO3cQBJ9SRQpu/lvE8WS+jCQXXVAyylaUu T68TXQubsNNpZK1YwLVsDV3mSSPSFCoPJeTH3rv5dvO2UiYtu1RpKxH0o4xvzNBHXuX0WnRpcGzQ supWlSyqZMjccfGN6FGG2+Iv4u5cW+ALxNLSoaZUfmwoc1Abq9Jrq3GsPtrbI12gcFNHaI3oJW9q 7Z9qbEpSpapg+NA2ezHnJ4rZxLCrZLVshHYWzdutBXPECCoAjzqRtlKxNpxp22U0tSTLS4kCPXen +XLW5xR2EN3iXE/SCmwUj3wD9tTSsHLPdUtK1cYiaDnq1bXa4jcWLuy23FAe7b7q616v1yb/ACFa JMyySg78INc19IViLHNAuG0hPa94jzTxo79Vq/DtjiWHDcJWHk+iuXxFAXrls7xO1etJkCCaevNy NXOvGGxG6QDQaIbM8AactohMRWyWyD605aSInwoGwTIgCD40MOsa3/4cXh8Hm/1qLSkhMqNDHrHN T0Y38Dg43+sKCd6k4H4EHxDj36yKyvOpTIyZpJPF79dFZQVLr/T+Dtt4DEmf+Cuk+g1uejfAzEk2 wn4ml+v8EjK9sYknE2Y8vmXKT6BQ4OjTAyT/ACfh+caAiBoDYCaxxOlAGnjSralQJg7V4+sdmBtQ Rd2gaZM7nwqLdShCzE1NXS2wmJmot5CSdSQregY3KthE+kUNOnjtF5Hf7PVHatlfoFCim+hOkwDI 86p3SRhyr/Kt/apaKipklIHGRuKDmjKV81ZZuwi6cWSlNzpE8BtE/EiuqLbMPtFi2WVDVA2rjXEy 5bX1skApW2AQPA6ia6awJQNvavpSZWgKI5EEUBLwmwbxFSHMQYZd098FSQQD761GMWuK45eYPhYT cKsUJNw4ncIKphI84BJqs5hzKcPy4+q0lVypISNIkk8gPEkmKnsuZCvMHyEi3sMQFnmC6V7Re3Rb 16nFcUnyTwHp50Epmxm3ssnrYeUe0uEHSIoIYg0cKS9cOW5Uw1ClqHEJ5n1q59JWKYhh7tthN0+u 7et2NetKY1pHExyoft4lfYnir1u6sPWL+mUqT9HxE+HxoLhaXS2sND9k5rbdROpJ2INa212OzK1L nn3qhMtMPYU67hThKmEElon+YeHw4e6nFxPblQBKPyY/x5UAt6VLwPZoba3DYbn3knf6qLXVGbcV e4u8QezS02kEjmSTFDHNuXcRxnNDKrG3U4SnRJMSZ/711B0I5OcyhktuxvA37a4suPqRuJ5CecCg ujoEQfCtmOI2rZ0d2vEpEbcKBy2AVcvfS6UgbDnTNJPnSrRK5UmRQKqC9ESDvyoY9Y4q/FlfiQfn G/1hRPWogaYihh1joPRhfEj+Mb/WFBMdSon8DgDyL8emtFZWvUnCRlFYSRIL0+utP7IrKCr9f3/R q2PH90mR/uV170EKKejLAxAP+TbfpGvOv+CMsWpPPEmY/sV1r0Bk/izwPcx7Od/zjQEhlRUNk+6l XEymY5UmyDp4x50uggpHemgj3mZSSRA8aadmEqAVFS9yfmzHhUU8UzvyNB4tpBG4NMLy2YeZWFN8 QeNPA4QdMkT9VIvOHSUhQ3oORulXJl7a5tvbq2tyq3U4FIQBvBAO310bMBsQcKw+4ZIIS2kKSR5V KZ9sPaWm1WqW37i2lSxH0gBBFUTLeamLW+Tg97cBt9StSEcAB/N9aAws4LgLCbTEbrSi3aWLo61d 1Kk7ifQ7+6vXeke2uLdCsvYLf46tThSk2zKuzBH85wjSBVeTeu3rCMPNom/tnFauyUdKfzvETyqb fubpFs2wthtCUCENsSAPACKCk50xPMvt72L4nkt4Xa2SygtuIUkIIjeFHkfKh5huMWdk+W73Bb2y UDu6WSpER4iYq45rwS9duV3Nz7Ssap+ceJ5cAJquM9o2QEIJ7wkKPCgnsHxCzxa1F0w6haSkplJ5 UjdH5tdwe4gCRJj/ABzpo0lq3X2rbKGVLBBKe7J9OdUnpQzULWx+R7R3/KHk6VkH6CefvNBMdEmc 13PSMm1u3W12C3VBgFIlJ8Z84+uuvLIzaoKtyRXzpy7fXFjcpWypTa5ACk7Eedd99HmJt4zlHDbx D6XlKYSFrTwKgN/roJ1xHdFY2J3mt3EmIrUCO7FAqhAVsRtThIAiCD5UigGCZO1bpSqNiaBR3SpH CCKFvWNI/Fne7TLjY/vUTl6tJoXdY0KPRnfGP41sf3qCb6lP+h7h0gDW8J/PTWVp1Ilzk59E8HXf tRWUFX6/5P4NWoKhHyizt/8AS5SnV+bDnRpgaU7xb8PzjSH/AOQNUZfsk+OINH/dOUr1eFhHRlgg 3nsJHl3lUBONvGxTBitUthPGZpVLx3kztz51p2xJ2igTuWu6ajLhvjvUs8XltEttqWI3IrZGFJWl K7l4hSk6uz578KCtLQrXEz4RUknAHT2a1uaFkayI29KnWcIsrZ/tN1rSJgqmo1u5vLpt1p1pTUPE srk7jjvQBvH04hhuarxzELlaGn0Q2hHBJ3gTXOefrwt5oadUl1p9h4HQvYxqkGuoOmF+1U0bJ25Z aeWsKMqEq8QJrm7F8PcxfPTLKy12LSzD6mtaFJBkBQHHwoDBaYld4c004SpbRAKVDiAeVWFrPNi1 aAugrlOgEHcbc6jsv263sBZtbhse0sICXE6eGw5elRSsJtA4/rag8Y+6gaZqzheXlyvsSWrdHdSh KREcKgm8bt0IlxYTGxkQaZZvtW14u0D80yGwVRtJBqCxRy2ClOaU7bJkzvQSmI5mff2YgNjYeJNC /MTy38auHnFd7VzNWW9uDDVtbgLed2QmOE8SagMesfZr0NKJU5pBVvxJoNMLt3HvyoI70zw3/wC9 dP8AQRnVzD7K3wm5uGlWSTpSeaT4ek0CMm4GL68asw65LpCNCB4kcffRz6JMAbX0iXOEBxsJsWUO IhI0qBH+PfQdDsrQ4RpcSraYml9AUkq2EVDW2GO2OKe0hsr1JIUUq2I9KmEPs6ezSpJWRtNAow2C jma2TqA7vCvGFOCQpPpW5PmRHGg8UCUgn30LusgI6Mr8CZ7Rs/3hRWOyBznwoXdZRMdGV/wMuNfr UDvqQz+CNwTw7Z0Ae9FZXvUiH70LiOAedjb/AGdZQC/riY9cZgybaXdx2QWnEGUKDaFIAUGV6tlC eYq1dX1BPRrgRHO3/wCY1WuubhNnhOUrC1sX3LhtF61LjiYUo9k5JMVaur4kDoxwLf8Ak5P95VAT A0rmB8a2w+1L9ylomATvHhSrTetMJBJPKp7B8P7BsFMdqrdRPKg8xK0Qzh6bdlYbkiSBvE71AnCc TxHGRcqAtrdteyie84ANtuQq6KZSmCoaz4mtVpII22oIe2wq2t2tMOOL3lajJ3qDzc37Jhb2lSkF YgL/AJm3GrhpUkn3VSOljMWDZawBy5xd4gOgpbQlGpSj5Cg59zRaP67w35beeZAUwp8gl1JO59QK p/RVgS2XL/GL5am+3LyWg2jVI4QBB47/AAqbxvNb2a1vMYQ+i11KSEQ0QVeEq4/DzqwdGjLlvlkL unHEPquH9bkAqRGqVAcyDvHlQXHEMETbu219aJKA+yhtxB5EDYn7KrGYLY21yEOoLayCFBSYNF+8 w4sXFu+EqubNTRVp4qVpbBA8NzJk1TukZeMu4fdN4ZgaMTcaLIDIZDim9X0zMgwPf6UHP2e03Tt/ bN2rS3HFpACW06lKPkBxppYdGmasQR7RiCRhtvpK4cV84QP6PL311ecm2jFs0xgybSxu1sIcDxZk lJ4jYgmD9tVvFsoWiQ45mDHVvW8QvfsEQDChtufpJI35Gg57Tl/C7ZDjdtcsWz7SE9s65K3EhXPh z4eFVvNtrg15b9ph63W32nEMtoKCS8Oa1K4DyFdE57s8k4060zqQbi2SG0sNgw547jYgcffQr6RL Sywu6ZFtbN2zCXO0LaR/CECdz/jlQVnLWH3RxRoWoLEaVLUtUwf5225H30XeifGlYJnO0tn0ds3f /MG6UgJgkyD6HhVUyW3b3l+wEpSxcusjSDJC522+HD0qy4Thq8VzrY2QcKmRqaeCO4oECAoBQmAR QdTWTYCAVLCieFe3WFWV4pKnWh2iFSlSTBFQfR85eHD3MOxLvXlkoNqWf4xP5KvfVwZbIIOxmgi1 4elgSkrIPDypqtlQE1ZnW0qbg7yKjLxlTKVAJGmOJoItepKR3gDFC/rJKJ6Mr2AdJcb3P9aig4VE bDj4UMOsekp6ML2SILrUfpUEj1JmyjJrytiFOOnj5oH7KyvepPIyfcTzccP95IrKAb9dN9p7J1gb a+urtkXjPefIKpDTgIMbE93iKtHV1LKujjL5fC+z9mIMeSlVS+tZh2L2HRrZt47Zi1vTiTQKO11w OzcgzJnh4mip1YcNb/FTgDrqQrVbE7jh31UBOsLcdhrbZ0JiEDnUvbM9miIk86SsdK3VqaVqbTtE bTzp4J5UGq0kxArRxsngDTkkgbcBWizwmRQNXGjvFBHrLZYxXHUYabJhbzDetC0gx3jEb8uHOjq4 ElMzNUHpcfScEFmq5RbofVC1qXpgDfY+M0HI/YYtl1a3bmyt7C3LnZ26zHarIMaoEmOfhRKyM8nE 7Fm4tihLaFKWHNMBKtCiVkb89yKHXSo7a2+IlouqKWkKTDjsjURsB5wZnzq+9ByG2cCtmlKI1rB4 mSSOBI5GfhQG51SMLwhd9d3wWtNqhaU6YTAG+mPEmn2AN2tvZm9cCW37sh1ZPiYAHwgU6Yw9rEcv 27ToAOgBJT4D9hpZGHNuhDK4T2YiOQoEsVtnlLtvZ7QOuIUdLmqNCSNxHP0oZ490dXl5jryPlJ9F m6kOOslZWFKPE7kxwG1EZnGGnMSvLNKoatEEvOzEE8BNKNOYa7iSk2zrbtwWwpxSSVGOUnnQU+wy Rg+HIQpNqhSkc1JGxoE9OeErv82Iw9huO9Ko2CUwkn7a6rv7VTiDpMeBP21zn0j26lZuvLy3e1Ia CVSonhI24eCaCjWtk0xcW5xC8RbtWoCmm0nvEJMgGOG9WHo4dxRzOthdXGHutoQ+oF7V3QF8IJ3P ED31EvM/InbEqZccvHNRURqCWwIjf3mpfJ79xdZywtCriXC+lxSNR7yAdyYMAbbCg6WbZXa4na4m 2SEqhm4HLSfon3GPjVub2G1QVtbC6wxbau6FoInwqTwl0uWqAsy433HPUUEnJg7CKbvoC0qB4U4k cfqpFwlSjHKgg7pstrIKNPhAoSdZhSm+jW4kDvXLSfrP3Uar5jtGpmKB3WkXp6O3GuZu2p+ugn+p aQrJTp0gaXHUkxx7yT+2sr3qWQMjL8S69z/pJrKCqdfhR/B21bIgG8ZIMcfm3Pvq79WMA9DeXdWw 9nV8A4qqf19Qn8F7NYBKxetD3FDn3Vaeq68X+iDA0L/ikKSNuI1E/toDCNKW+7tXjbknTMKB3pNR BRusSRwpG1bQlZdTqle5M0D8pIRIMzWi0k7kVs26VARuOHpWy9kydtqBvyAoR9PDdy6W0NBOlLJV KuAMnf3UXlQTx40JOmrHLe1vfYLllLluGPnSeRPn8KAA4sxhGY0IwXEbdAcaKSxfJRpVpJAjVzTx MHwFGPLGXLXA+yw6zh1lgobSswZKYG9B3GUWzCEXdmXHGFPoShKR/Bq8FetHnJTbjlip1/6Yuz9L YjvJP7aC9ZYYPyUyoOiQkApSQUp8hG23Davc2JxC3wi7fwlkvXvZHs0AgFR8idp8JpbKaAjCGwmS EqKQeaoP0jtxPH31LPJ7VlSRsY29aClZTwFdnkq3axBhab19IuL8FYUpxziQo8Dvy4VL4Szh7XaN WduGlIjVCCBvy4fZUi2T2imjsJmDSVuy6h59bjoUCrupH5I8KBtjTybTCLl8qCSlohJP84jb6656 zk0FX+IS0pbhW2lKUnc7EzRw6QHpwL2clZ7d1KShIOpQG8SOA2Ek+nOg3iuFrxBV+Ge0S4HHXAAd +4Dtz5GgodlhTuLOXV9blt+3t1qTcIKtK2FA8DOxnl76neizALh3NTd4ppRJCVIIJ+jJ+HCvcq4h 8nZdRamwLpecU4tLg1BaCd9W3Ab8TPCp/oVxC3s80Iw95d2lVwSlhLoEJgcoHODQdA2Dei1QmDJG 9LJZ7N4vN7H8pP8AOrGNSedOAQU7+NB6l1KyIO8SKb3L4QstEwTBn1P214NFvreIJK+fgPCoyzWi /eXfPhaQh5SG0qVACdoMefH30Ek+4lCi2dwQTx50Detesfi6J4TeND6lUYbgqf7VsK0KSe799BDr VOLPR8pCjwvWgY9FUFr6k8fgSuP/AFHj/eR91ZXvUm/0GV/Xe/XTWUFc695P4NtJO6ResHj/AKty rb1YRo6IMvAAd5lZ9/aKqqdexKjgCIE/5Vbn+45Vs6ti9PRLl0Dj7Or/AIi6AqO6QtC1qjkBHE1o HCEKCdlQSJ4DwmtL3UGwtKgkpM1s22OzCn1pJMcOFBtgq1rtRJKlT3jBAJ8p4ipIokAz7qq+FYnj D+OutrtbVOFto2eQ4dQVJATEQdoPvqypdA3kEcaDCxJ5jehL0k4JbX+aF2D4Kn7tslsaZ2jaD4yf qovJeBA9aGGbcYUxmS4XcLW2GwopMDToHL12nagDeMWrOQ8Ut38Te+bfeShTDYSrWUwZUOW870UM MxBD2EX17bgFKrp4twdjCEkfZQn6VMHxDMuZrB/CXbd3tnA0u2dX88gkzqG26efjtRiwWy7DLhY0 gr9ofQTp3nSdx+jQXTLLxUxdJVGpL6jtMQrcRPkR75qQQ+kKUkq3mq1ktSoeSlBQkttqEj6RKASf MST7wamXwWX+2EQdlDwoFApS7wmBoB2ptbG6TdvOOvsG3OzTSEFOnfz4+tN7fEW3seXZNmS0NawP yR51o9d3Vzj6mBZhq3aSfnCoFThkbwOAoIHON2HsXtbALWgBQVISd1KJgTwiEmR6VV8rgqexG9AB 0WjzgJG0mpa5Wm4zViN8hQcRaoXJ0gAdmgAAEHeFFcnjO3KkujlTSbK+uX/4MMLCpH5Ox4e80Alv 7O89rtLK0tlXK1EgISqEK27yp2HAbDyqU6MsLXhucEqWyHHGnNTbhSJIVPCCY9Kl8CtsYvbxzFuz bu7dLmi3t1PFGpO41pjhyEceNPMpl+zzkwzeKLr2IK+ilOlCAlJIgevPnQGFNwrsCofSArXA7x29 Sokd1KiJ9K2QzpSNUwRxpLB22MPs3xqVHaqWeZMkmBQa4u+3fONWdqVrLdylL+gwWiBqTPiOHuNM ct3D14L9zswi1RcuIb1cVadp9Nqe4241h6HMQbMLQytao4KhOxPjwpnktl1rKNqHge2cRrcP9JW5 +2gmrRsItgTBJ8KA3WzRoyUFJ4KvG5+CqP6GyGUgAcKBnWzQT0epIH8ub+xVBYepbIyQ2AQR8/Pl 84Kyk+pPq/A10HcBbo9O8jasoILr1ScBRBgh63j4O1YerOo/ilwHURs0sbf7VdV/r0j9wkqn8u3+ 1yrB1Z5X0TYFwjslgf2q6Aq3X+bSPCeNIYchNywQ5JTBETtTi4SVWy4HLamuEhSElJM78qB1CGG2 7VpCUpnvnkkePrUQ/cXjOYmbFAUbd4FSf6McR9dSmINLUJCCptB1KTzX4D0mqhmPH3MvZgZvbxtR sy+1bKITMLcCt/iAKAjhsJQIiYoW9Ilp+6V44yg3DhQIQ2qTJ2I08440TWHw/bJdSkiUzvQ2xxdy xib7rDalvF8nYctXCaAMYfjWJp6Q8Js8SY1D2xBCwkoIIOwE8eKZo34OV+zW0pK+1vS4QFcAttdD XGMYwu66SLG4xKzadvWLhDbLikFKgeB9QJPwotYez2YwxI4FDK9/zx+2gTynCb20WVhRfsQAmBKA hRmY33Kvqqw4mlCLRxxwEpSJgbk1XsHhl7D5b0Bu5fZU6Y4EylBnfcmdvDzq4ONpXG21AN8i4ZmH D845gfxLsBZ3LiV2znaa3FbniD9EAbRw22qw2bTFniV865dXLj4BUpK1Hs+E90UviDqm8UJgCFAH zBH3iorPVwm1wdy5ceLZdSGUKClJAUo7GR/jhQVxntG8v4ze3LTSHnWtB0DYFaiYnme9x58aQwNt bWT8XdSrQPZwkq8CZn6op1i/zOT20JVPbuBSTxkAEj7BS+VOweyhiDlwvTbuFxKlEcEgQf20AJuF Y1eW9vb2jtxpYK3FqZdG0ERPDxiiV0WYWu8xNGP3ilpdb7gSsGCqIkVBXgwDCMFQ40t5Srx5QZd1 hSVRAkDkB4bVa+iS+YcuHMMR2kBHtACySdzvx5cKAj3t8pi1WUtqWoJkAVC/L9naWKbh1Sbi8WUM 9i2QopWQYkDcDjvUvi94zZYY/dOphttsqJAnYChDlN22ds14xiS/ZXrm+cuWme00rUClPZpUBvJA n86gvrz1zieBui4T2NzeOIY7IHZvUdwPQA1dLVkW7DbKYAQkCqlhlstzMOCWOgDskru3gTMHTpAJ 9VH4VeFp+cMAbeNBouNEBRG24oJdbFH/AIcpM8b5refJVG90d0bbmgp1sEk9Hze8RfN7fmqoJTqU 6fwKfE97tndp80VlJdSrbLFyJnvu/aisoIrrzpP4OoVy7S3+1ypjqvuT0SYIkk7B0f71dRnXlSDl hKo4Lt/f3nKfdV0a+inByPyQ6D69qugM6BLShHEUyw7a4dQDMHantuklBHKmjbfY3ijBAO9BviT7 bPdeMJcQQY/Z9dQV6S4n2e+CXgp5tbIUmSUpgyfMGneeGFuYCt5DikKZUlyQJ2Bk+u01st1tWYbS zCNZuGlKkD+DSIG/hMigstuEhlO0gjaqRmppJvXvY3GW7pSiYdMJkDjPjV8Q2EtgDkIqi52tmlG4 L3dE90zxJHDyoAnjDdwzm21uMX7uKXVwA2yIKUJTxWFDZRO3186NzjqWbXCVE8EIT8Ck/fQlunm3 834azcWVtqt3VJQtLkqEjf7d/Si9i7KU4HaPJ4MupmPAgj7qBtdtqRbX51hPs16h8o2GuTATJ9QR 5gVamHSS2VRCk+FRl9bj2m7HY9qLi2JCAY1KA2G+wM86c2F12+CW7y161pQNStpkcZjnQNcetwhZ dA7y1JHHjVTz5cLdcw2wYQHpd1OTuECISvbeZkDx34xVhz1iAscKNyYKUd4jVHI0PsOujfZgaxC5 uVIbZSFFtPNOkOBRj1IA8vSgks4OMpNtZtAFthWohJHArA4egVTjCbFf4txag6XHWCtfA7rOoj6z UFfF66XcXUavablQSTP0Q2qOJ/pD3ir4y2G8Hbti1rBbCVDgIAoOfsaxFOG481hC3LZ21QrtWm1t hayrgrSeRIMRwgUR+ibCWkdpj3cC32g2hCTPZpHI+e31VRs2ZYtHLxePKJUwpxTSWUcdYPDUOCSI 38iKu/RG4ub+xDehhhSVISeI1DfhtxFARnG27izW28gKQRBBHEUEsFwkXnTJcYeo6rSwWl9CI4bC J8d/so3gAtQKpOG4a3adIuL4i0nvOWaJ24qkxQWzKzYdxbEsVIOnULdonwTx+s/VU8rvKK5nwApD CrX2TDmrfYECVkCJJ3P106aSJ8qBEqIAEDhQZ61xB6PmduF83+qujY62nwoJ9a8j8XzQif8ALkfq roH3UsP72LoT/GO7e9FZWvUtIGWn0wZK3if0kVlAy68IJyxO8Tb/AK6/vpfqquBPRfhQJjvuiDz+ cVSXXg3yuE+HYH++uvOqopA6M7DWB3XHY/TNAemBKCBSdy2lDqSY350lZ3KdxIn7a0xJ0ltCkmCn eg0x1CXMJfbIiUECee1UToGzLe461jFtibRVcWF4WUPkfwiOQnxEVaMRxGbRYWZEGo7oktLS0yym 5YA1XTzjyz4kqNBeytU+NVvMFlbYip5t5TqVEBIUhUaTyPhU4t4p4c6qGbHL9DinLI/OIlQSTHLj QDfOeCsYNmBDlpZEvKR2i7kypSjqg78Ad55UUFLL2WlIUkmWwRtPDehPjeM3t04W7nuBtxsFA2JK l/ZANF7C1JVh7bSu8kpjc0Gl1iLbVrh12tQRKkoJJ2hXdH1kVtgbgFvdWyne1LbihqkHUPHbn4+d QePYbcYng7Fqw5p7J4FSvNCpA+IFLNXTeFIxS+dSUKTal5QJ2UUpMke/9njQD7NmNX+dM7KyvhyU 2+HWw7R+4c37VKFaVCBwEggTE0/dbfQWbewabsy6pWs9nJDadjII2Jnlt4VZzY2eH4cvFbawtkYl fNN+0PBAG8TJ99Nra0Q7jTSyhLa3GktrK3ZEDcpTzJkn4cqDW8swi5wu23AbaU85pHFS1AD6pq2L HzACRAAqKdQh7FiscilA9E//AO1KuL1IMcAKATZrFthgdwnEb1NjhhJdU+133DqV3U6dJj135VNZ CxrK7PZ4Xg945erMBb3YmSTJ75gR68KpvSFdtXV3ibz60e0W1ou5LSRsEAqS2k+oMnwIqMyLauYM 3heI3lk9bKfulWt20pWkgLIgn3EEUHQEd07kCq1lwPO5qxRTgGhCkBJ8QBP2mpe2eXZdnaP6ltaQ lp8qmT/NV4Hz5+tQVveLtMcu9AkuOoB9KAgNqUobEA1oHFa9JFJ27mod0+tK7SFzvzoN1lWmZ4UE +taf3gsTzvkAforo0uujSQN6B/WqcKskWyRzvk/qroJfqYT+DTuw2U9+sisrfqZD97D3kp2fXUis oGHXc3y3Eb6WD/vF1TOr7mvCMJyFbWt3i1nbPJdc1IceCSJVI2NXTrrtzgIUebbH1OL++uNnEkHa RvQd42PSFlkAFWP4bI//AJKPvp09n/La298cw0+l0j764C7yTxPrXhKualUHcGN51wJVo6pvGcOJ 0HhcI++k+jDOGCYZlJi1uscw8LC1qANygEBSiY4+dcSHUocTvXg17gK4UH0Jts/ZXUolzMOGJA8b pG/11Vcx59y6rGJbx2xcbKIMXSAkHj41xBqXEaj8a1UTO5NB1FjucsuqddGH3tj3HWe0V24AUe/M c1RIPGN6I+HdImVm7VtCswYYCEj+Up++uFIJ8ayFfzj4bUHdFp0j5TQ++hWYcNSgr1g+0p58ai84 dIeUVWi1s47h90lbKmXbcXAhaVbevjw8fSuLAFR9I153juSZoO2nek3J5Nuk5isAlpIIT2oIGxHv 2Net9KOUWFBacdwwlcqWQ6JnhwiuJAFExJisg8N6Dta16UMn9sFuZhsATqO7nifup4elTJn5OZLD +1rhzSrzrIJTtsaDoPpMz1gb+NG7sL21uBd4Y7au6FA7laon0mansdz9lrEsh2S14tZ+2pQytbYW NWtIAPv2rl7SRsTNbBJ8aDtW36UMn3Fg2HcespU2nUC5wMVBPdIWWk4s1+7to4nWklwrHAHnXJAC toJrfccKDuWz6UsohO+YLEf/AGU5b6UMnkE/hDYE/wC0rhi1WG7hC3UFxCFAlMxqHhNSPttq46Sq 1S2kuqXAPAEyEjyHCg7Vd6T8omNOYLDf/WULOsDmzBMeyqzbYdidtdOi7SvQ2qSBpUJ+uufFLK3V rQAlKjIA5DwpdqeZJoOu+pjtlh8Rtqd/WTWUr1N2+zyo54rLiuP9NI/ZWUDLrop1YAgaTu21B5fw iv8AtXHrzMmNJ25iuzuuIytzLgUB3UtN/Hta5DW1J+jQRBZAMwTWBid441KlqTFZ2IkkpE0EZ7OJ 3FeKY7xhMVK9l4CvOy24cPKgiCx5D41qWoMQRUuWSREDjNeFkapIoEEi0Fro1Q4EJAIRzBnf69/S lXrlpSrwtdnp1ksgtjgVSeXhtXpYT/NE16GeGw28qDxm4bLlmXez0hep8dn4K25eG1aBVsm00KIL obUmQjiZOx90QeVO2WmFFKFMjUTx1QPftTj2FggkBgDURBe3+ygi7z2Zxp/sVJSlTiS03o3QnfaY 9PXjSeG+zNsuJfSky82RIJOkTq5c5G3OpP2JorCAlkd0GS5sd/t+6tnbW3aX2iW0KSFboS7Mj4UE Y8m2DKezUndrTu3BCtczw8K2U1a+03L6XG9Dpc7NGg93+by293hSzrLanCW29KTwSTMV4liOVBHP WiUL0ocS5wkpBiffSaWNztvUr2G86a9DMDYR40EX2BE6RNbJYPGNvGpMMA8q9LJ8B8KBghnfhSoa QdwmDTzsiTukVuGjEQAaBu22EwIpw0gTsK3S0rbu07tmVTPCg606nrShk5bihA1OJHn3xWVKdUtC R0bJUEgEPLB24nWr9kfCsoNOtVaOXeUEttNqWsplISkknStBP1Sa5NXhS9KdVncSAdXcO/hX0PxG wssRt/Z7+0ZuWpnS6gKE+O9RRyblgmfka2HpI/bQcDDDGAynVZ3Xac1Rtz/7Ui9hqVbtMOpE7yJr 6ADKGWxwwlj4q++tGcmZYZWtbWEMoKzKgFqgnxiYoPn/APJrv/oufCvBhywf4Je3ka+hP4L4BEfJ jUep++vBlfAAZGGNT6q++g+e3yavj2S/gaw4a4Y+Zc/RNfQo5XwAmThjU+p++vPwWy/M/JjXxV99 B89Pkp4meycj+qa2GEvT/Aun8019ChlfAASRhrUnzV99bHLeBnjhrJ+P30Hz2ThL+/zDv6Ne/JFw Zlh39E19Ck5dwVPDDmR8aw5dwX/49r6/voPnl8kvD+Idn+qa1+S3Z/gnI80mvoactYESScOaJPmf vrwZZwEGRhjIPv8AvoPnmMLdH8W5+ia9+TF7y0ufSvoYct4EeOGMH1BNe/g7gn/xzP1/fQfPA4cs H6Cx5RXhw9QH0VV9DXcsYAv6WGMn4/fSasqZdJk4Wz8VffQfPgWPPQfSsNlvOk719Al5Qy2RvhLP xV99aLydlk8cIY+KvvoOChZ4fKtSbgbbcONYLO030h7yMV3j+BuWNR/ce34+f316jKGWwSBhLIB2 O6vvoOE7e0swiXRcSBtoAiYP7Yq69FHRxdZ9xt2zs1qtLW3QHLi5WmQkEwEj+keXoTXXyMpZcSNK cJYA4wCfvp5Z4DhNqoqtrQNEiDoWoftoGeRMrYdlDBk4PhfaezoMhThBUSSSSSAPGsqfSAlISOAr 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-- mu-1.10.8/lib/tests/testdir2/Foo/new/000077500000000000000000000000001452323006500172065ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/Foo/new/.noindex000066400000000000000000000000001452323006500206410ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/Foo/tmp/000077500000000000000000000000001452323006500172155ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/Foo/tmp/.noindex000066400000000000000000000000001452323006500206500ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/bar/000077500000000000000000000000001452323006500164365ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/bar/cur/000077500000000000000000000000001452323006500172275ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/bar/cur/181736.eml000066400000000000000000000040741452323006500205040ustar00rootroot00000000000000Path: 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 mu-1.10.8/lib/tests/testdir2/bar/cur/mail1000066400000000000000000000027721452323006500201650ustar00rootroot00000000000000Date: 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. mu-1.10.8/lib/tests/testdir2/bar/cur/mail2000066400000000000000000000006461452323006500201640ustar00rootroot00000000000000Date: 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 mu-1.10.8/lib/tests/testdir2/bar/cur/mail3000066400000000000000000000036131452323006500201620ustar00rootroot00000000000000From: 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. mu-1.10.8/lib/tests/testdir2/bar/cur/mail4000066400000000000000000000020561452323006500201630ustar00rootroot00000000000000Return-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 mu-1.10.8/lib/tests/testdir2/bar/cur/mail6000066400000000000000000000010621452323006500201610ustar00rootroot00000000000000Date: 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 mu-1.10.8/lib/tests/testdir2/bar/new/000077500000000000000000000000001452323006500172275ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/bar/new/.noindex000066400000000000000000000000001452323006500206620ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/bar/tmp/000077500000000000000000000000001452323006500172365ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/bar/tmp/.noindex000066400000000000000000000000001452323006500206710ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/wom_bat/000077500000000000000000000000001452323006500173225ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/wom_bat/cur/000077500000000000000000000000001452323006500201135ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir2/wom_bat/cur/atomic000066400000000000000000000016601452323006500213150ustar00rootroot00000000000000Date: 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. mu-1.10.8/lib/tests/testdir2/wom_bat/cur/rfc822.1000066400000000000000000000032451452323006500212070ustar00rootroot00000000000000Return-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-- mu-1.10.8/lib/tests/testdir2/wom_bat/cur/rfc822.2000066400000000000000000000026611452323006500212110ustar00rootroot00000000000000From: 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! --=-=-=-- mu-1.10.8/lib/tests/testdir4/000077500000000000000000000000001452323006500156745ustar00rootroot00000000000000mu-1.10.8/lib/tests/testdir4/1220863042.12663_1.mindcrime!2,S000066400000000000000000000143251452323006500220670ustar00rootroot00000000000000Return-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 mu-1.10.8/lib/tests/testdir4/1220863087.12663_19.mindcrime!2,S000066400000000000000000000071361452323006500221730ustar00rootroot00000000000000Return-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. mu-1.10.8/lib/tests/testdir4/1252168370_3.14675.cthulhu!2,S000066400000000000000000000016331452323006500216100ustar00rootroot00000000000000Return-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. mu-1.10.8/lib/tests/testdir4/1305664394.2171_402.cthulhu!2,000066400000000000000000000011561452323006500215420ustar00rootroot00000000000000From: =?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. mu-1.10.8/lib/tests/testdir4/181736.eml000066400000000000000000000040741452323006500171510ustar00rootroot00000000000000Path: 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 mu-1.10.8/lib/tests/testdir4/encrypted!2,S000066400000000000000000000045231452323006500201420ustar00rootroot00000000000000Return-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----- --=-=-=-- mu-1.10.8/lib/tests/testdir4/mail1000066400000000000000000000027721452323006500166320ustar00rootroot00000000000000Date: 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. mu-1.10.8/lib/tests/testdir4/mail5000066400000000000000000001322261452323006500166340ustar00rootroot00000000000000From: 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 w8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/ANNRzUiIfSlR DmrUUfrXOajEiz2qZYPap0jqlrOr2+i2RkkZDMR+7jLYJPqfanYCd4OOlQGe2ibbLcQo/ozgGvPL vxRqE6kSXsj+yfu1/JeT+dYsl55m7JUZ6kD+vWiwrnsAlgfG2eI/RxUhjrxlLhgMLKcdua0YPE2q 2mDHfSMB2c7h+tFgueqeVimmPiuO034hKWEepW4APHmw9vqK7G3ube9tlntZUliboymiwXECCgpU oSnGPNIZEqgUVKIz3opAVUXDVajFV4/vVT8R6yNC0SW6THnsfLhB/vHv+AyaaApeKPF0WjK1nZ7Z L0jk9RF9ff2rzC71Ge7naaeZpZWOSzHNVZriSaRndyzsdzMTkk1GFLHABP0qhEhnc5xWjpmg32q5 8oY9M96hs7DzHG4MBjJJGK9j8HW1ta6ckqqHVunHQ0BY8wl8H6hDGWOcjttNYtxbzWxIcHivoXWN X0nS7YzXh5I+WMDJNeY+KTb3umHUotOlhgkfYjkfqaLhY4ESZrb8Oa/caLqCMrFrZyBLH2I9fqKx Y4mkcIilmJwABkk16R4V8BmDy9Q1dfnGGjtj292/woCx2qjIB9aftp+MUvFSMYFopxopDKCDDdK5 L4i2V9dWdlJb28kkEJcylBnaTjBP68116ctWjanFUhHzpit7SLa0t1W7u5lCk4AxmvZbzwvoGoOZ rnS7dpDyXUbCfrjFeZ3WkW0OuXEcMMbQK52RljtA9jTESWNvDqExntAJDu2YEf3R6/413Xhq7itb IW88MgKMct2zmvN47h9C1WCS1uzGGbbIqnPynsexFelLMloUMjF4pVBEh75oAvX50q7+RIXknkwG Pt6c9q0bu00e/wBEk0m/kht4pE2KrOAwPYjPcVmWlza2TSXTbWwMgep9K5zWdavNSmISzS4bOMjA x6AUhm3pHhfRNFVZLG3EkmOLiQ72P0PQfhWnITUVjJNLYQPOmyUoN6+hxUjZzQAwr7UgWpKCMUgG 7RRRzmikxlGNavwDiqiDB6VegGQOKpCJygkhaM9GBBrzHxFp50+5la8tnO4DbIg6LnqP0r0S/v5L K1MtvaS3bZKbYcHDDqD7+3WvONT1u+1W/jbUSVtY3yYEGAB396YjCtLQXt1mOBhAjZDPyT7V3GkX bJbtp92pe1I+V8ZMf19qzta8T6dounx6dplhbySkbi/U4PIJP9K4y88Q6reYMk2yPOQiKFU/h3oA 73VLC6trFrmCVJ7dTn5X6fhWLo3ih31i2N3Bi1iYqWjHTPc+uKg0zxCb+2NrNaHj75hOFI9wKs6j FF9hf7CotmjAZWTj659aAPUQVdA6EMpGQR0IpjCuJ8GeKVNtJYXshd4xuiKLyw7jH9K7SK4guGdI nBePG9Dwy5GRkHkUDFA5HWnMBikIwaKkBo64FFAPNFIZVQVi+IvEk+mSJa2OVnxueQKDt9AM1uov NedeIJkufENyySEAEIMHjgY/mKpCZaOri/hjg1G3jnhRtylB5LofVWXHP1zWpqF1pt1pg8+4jmlS PbA6o32lj/02ydpGOMjr+lcsXliHOJF7jHNQSyAYmiPyA5I9DTEQnTVimMwywHOD6VoafJHp13Hc pZW92gJIhnGUORiljcSJnqDUMTBQ0fdf5UAaFhANDj1STUbP7PJN+9EQHHlnJUKPTJNSW4G4jqCB 1qKKzu0EeoXV0k0N0CqiSXc4C5HI9OaSNsXCIvCg4oAzNbj1O01iDVXhS3km+dPKUKpHTOB61b0j xNLYayl3JGBE6COWNOAQOhFMv7QGCa7N5E489kFuZCXTHOcdhWMGBJJ6CgD3BJEliSVTlXAZT7Gn AA1xvgnXJrxX0+5fd5SAxEjnb0wf0rshjFSMTABopwxRSGVgDn615TNCTNMrctHIwP516xzuGK8z 1FBa+I7yJgNrynH48/1qkJleOZo0GMstMlDy5MKxsx6g8GpGUxSnb9084p5RZUyuFb06UxGfazPE WicFWB4B9Kk3fv8Ad68VDOri4BaNt443D+tEchKsfegDQsVQy3DjJcR/d7dR+Va9lDGLR/NkLOzE gAenTn6iudsJtmoMeoaMg+3IrbtpJVjXa+O/NAGK4Q3M5bG7ewz+NZqAZcdlY5q3bxXOrasba0QN LNIxUFgo7nqa1PCPhttZ1a6S7Vha2rnz9p5Yj+EGgDLsZ7i0nS6t5WikU5BHp6V61o2pxatp8c6M vmYxIgP3W9KI/Bukyu0stmgZkwkSsVWMepx1NcOss/h3Wp47K4WRUfa+4cSD6UmM9IGM0VQ0zVrf VLbzImxIB88Z6r/9aipAn3ZNcB41tGj1gzKMGVFcH3HH9K7pDzXJeNCrTRuzHem1AueMNuOfzX9K 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-- mu-1.10.8/lib/tests/testdir4/multimime!2,FS000066400000000000000000000011601452323006500202470ustar00rootroot00000000000000Return-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 --=-=-=-- mu-1.10.8/lib/tests/testdir4/signed!2,S000066400000000000000000000024151452323006500174140ustar00rootroot00000000000000User-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----- --=-=-=-- mu-1.10.8/lib/tests/testdir4/signed-bad!2,S000066400000000000000000000016611452323006500201420ustar00rootroot00000000000000Return-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----- --=-=-=-- mu-1.10.8/lib/tests/testdir4/signed-encrypted!2,S000066400000000000000000000044011452323006500214040ustar00rootroot00000000000000Return-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----- --=-=-=-- mu-1.10.8/lib/tests/testdir4/special!2,Sabc000066400000000000000000000005361452323006500202330ustar00rootroot00000000000000Date: 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. mu-1.10.8/lib/thirdparty/000077500000000000000000000000001452323006500151625ustar00rootroot00000000000000mu-1.10.8/lib/thirdparty/CLI11.hpp000066400000000000000000013236741452323006500164640ustar00rootroot00000000000000// CLI11: Version 2.3.2 // Originally designed by Henry Schreiner // https://github.com/CLIUtils/CLI11 // // This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts // from: v2.3.2 // // CLI11 2.3.2 Copyright (c) 2017-2022 University of Cincinnati, developed by Henry // Schreiner under NSF AWARD 1414736. All rights reserved. // // Redistribution and use in source and binary forms of CLI11, with or without // modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // 3. Neither the name of the copyright holder nor the names of its contributors // may be used to endorse or promote products derived from this software without // specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #pragma once // Standard combined includes: #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define CLI11_VERSION_MAJOR 2 #define CLI11_VERSION_MINOR 3 #define CLI11_VERSION_PATCH 2 #define CLI11_VERSION "2.3.2" // The following version macro is very similar to the one in pybind11 #if !(defined(_MSC_VER) && __cplusplus == 199711L) && !defined(__INTEL_COMPILER) #if __cplusplus >= 201402L #define CLI11_CPP14 #if __cplusplus >= 201703L #define CLI11_CPP17 #if __cplusplus > 201703L #define CLI11_CPP20 #endif #endif #endif #elif defined(_MSC_VER) && __cplusplus == 199711L // MSVC sets _MSVC_LANG rather than __cplusplus (supposedly until the standard is fully implemented) // Unless you use the /Zc:__cplusplus flag on Visual Studio 2017 15.7 Preview 3 or newer #if _MSVC_LANG >= 201402L #define CLI11_CPP14 #if _MSVC_LANG > 201402L && _MSC_VER >= 1910 #define CLI11_CPP17 #if _MSVC_LANG > 201703L && _MSC_VER >= 1910 #define CLI11_CPP20 #endif #endif #endif #endif #if defined(CLI11_CPP14) #define CLI11_DEPRECATED(reason) [[deprecated(reason)]] #elif defined(_MSC_VER) #define CLI11_DEPRECATED(reason) __declspec(deprecated(reason)) #else #define CLI11_DEPRECATED(reason) __attribute__((deprecated(reason))) #endif // GCC < 10 doesn't ignore this in unevaluated contexts #if !defined(CLI11_CPP17) || \ (defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 10 && __GNUC__ > 4) #define CLI11_NODISCARD #else #define CLI11_NODISCARD [[nodiscard]] #endif /** detection of rtti */ #ifndef CLI11_USE_STATIC_RTTI #if(defined(_HAS_STATIC_RTTI) && _HAS_STATIC_RTTI) #define CLI11_USE_STATIC_RTTI 1 #elif defined(__cpp_rtti) #if(defined(_CPPRTTI) && _CPPRTTI == 0) #define CLI11_USE_STATIC_RTTI 1 #else #define CLI11_USE_STATIC_RTTI 0 #endif #elif(defined(__GCC_RTTI) && __GXX_RTTI) #define CLI11_USE_STATIC_RTTI 0 #else #define CLI11_USE_STATIC_RTTI 1 #endif #endif /** Inline macro **/ #ifdef CLI11_COMPILE #define CLI11_INLINE #else #define CLI11_INLINE inline #endif // C standard library // Only needed for existence checking #if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM #if __has_include() // Filesystem cannot be used if targeting macOS < 10.15 #if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 #define CLI11_HAS_FILESYSTEM 0 #elif defined(__wasi__) // As of wasi-sdk-14, filesystem is not implemented #define CLI11_HAS_FILESYSTEM 0 #else #include #if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703 #if defined _GLIBCXX_RELEASE && _GLIBCXX_RELEASE >= 9 #define CLI11_HAS_FILESYSTEM 1 #elif defined(__GLIBCXX__) // if we are using gcc and Version <9 default to no filesystem #define CLI11_HAS_FILESYSTEM 0 #else #define CLI11_HAS_FILESYSTEM 1 #endif #else #define CLI11_HAS_FILESYSTEM 0 #endif #endif #endif #endif #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 #include // NOLINT(build/include) #else #include #include #endif namespace CLI { /// Include the items in this namespace to get free conversion of enums to/from streams. /// (This is available inside CLI as well, so CLI11 will use this without a using statement). namespace enums { /// output streaming for enumerations template ::value>::type> std::ostream &operator<<(std::ostream &in, const T &item) { // make sure this is out of the detail namespace otherwise it won't be found when needed return in << static_cast::type>(item); } } // namespace enums /// Export to CLI namespace using enums::operator<<; namespace detail { /// a constant defining an expected max vector size defined to be a big number that could be multiplied by 4 and not /// produce overflow for some expected uses constexpr int expected_max_vector_size{1 << 29}; // Based on http://stackoverflow.com/questions/236129/split-a-string-in-c /// Split a string by a delim CLI11_INLINE std::vector split(const std::string &s, char delim); /// Simple function to join a string template std::string join(const T &v, std::string delim = ",") { std::ostringstream s; auto beg = std::begin(v); auto end = std::end(v); if(beg != end) s << *beg++; while(beg != end) { s << delim << *beg++; } return s.str(); } /// Simple function to join a string from processed elements template ::value>::type> std::string join(const T &v, Callable func, std::string delim = ",") { std::ostringstream s; auto beg = std::begin(v); auto end = std::end(v); auto loc = s.tellp(); while(beg != end) { auto nloc = s.tellp(); if(nloc > loc) { s << delim; loc = nloc; } s << func(*beg++); } return s.str(); } /// Join a string in reverse order template std::string rjoin(const T &v, std::string delim = ",") { std::ostringstream s; for(std::size_t start = 0; start < v.size(); start++) { if(start > 0) s << delim; s << v[v.size() - start - 1]; } return s.str(); } // Based roughly on http://stackoverflow.com/questions/25829143/c-trim-whitespace-from-a-string /// Trim whitespace from left of string CLI11_INLINE std::string <rim(std::string &str); /// Trim anything from left of string CLI11_INLINE std::string <rim(std::string &str, const std::string &filter); /// Trim whitespace from right of string CLI11_INLINE std::string &rtrim(std::string &str); /// Trim anything from right of string CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter); /// Trim whitespace from string inline std::string &trim(std::string &str) { return ltrim(rtrim(str)); } /// Trim anything from string inline std::string &trim(std::string &str, const std::string filter) { return ltrim(rtrim(str, filter), filter); } /// Make a copy of the string and then trim it inline std::string trim_copy(const std::string &str) { std::string s = str; return trim(s); } /// remove quotes at the front and back of a string either '"' or '\'' CLI11_INLINE std::string &remove_quotes(std::string &str); /// Add a leader to the beginning of all new lines (nothing is added /// at the start of the first line). `"; "` would be for ini files /// /// Can't use Regex, or this would be a subs. CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input); /// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered) inline std::string trim_copy(const std::string &str, const std::string &filter) { std::string s = str; return trim(s, filter); } /// Print a two part "help" string CLI11_INLINE std::ostream & format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid); /// Print subcommand aliases CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid); /// Verify the first character of an option /// - is a trigger character, ! has special meaning and new lines would just be annoying to deal with template bool valid_first_char(T c) { return ((c != '-') && (c != '!') && (c != ' ') && c != '\n'); } /// Verify following characters of an option template bool valid_later_char(T c) { // = and : are value separators, { has special meaning for option defaults, // and \n would just be annoying to deal with in many places allowing space here has too much potential for // inadvertent entry errors and bugs return ((c != '=') && (c != ':') && (c != '{') && (c != ' ') && c != '\n'); } /// Verify an option/subcommand name CLI11_INLINE bool valid_name_string(const std::string &str); /// Verify an app name inline bool valid_alias_name_string(const std::string &str) { static const std::string badChars(std::string("\n") + '\0'); return (str.find_first_of(badChars) == std::string::npos); } /// check if a string is a container segment separator (empty or "%%") inline bool is_separator(const std::string &str) { static const std::string sep("%%"); return (str.empty() || str == sep); } /// Verify that str consists of letters only inline bool isalpha(const std::string &str) { return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); }); } /// Return a lower case version of a string inline std::string to_lower(std::string str) { std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) { return std::tolower(x, std::locale()); }); return str; } /// remove underscores from a string inline std::string remove_underscore(std::string str) { str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str)); return str; } /// Find and replace a substring with another substring CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to); /// check if the flag definitions has possible false flags inline bool has_default_flag_values(const std::string &flags) { return (flags.find_first_of("{!") != std::string::npos); } CLI11_INLINE void remove_default_flag_values(std::string &flags); /// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores CLI11_INLINE std::ptrdiff_t find_member(std::string name, const std::vector names, bool ignore_case = false, bool ignore_underscore = false); /// Find a trigger string and call a modify callable function that takes the current string and starting position of the /// trigger and returns the position in the string to search for the next trigger string template inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) { std::size_t start_pos = 0; while((start_pos = str.find(trigger, start_pos)) != std::string::npos) { start_pos = modify(str, start_pos); } return str; } /// Split a string '"one two" "three"' into 'one two', 'three' /// Quote characters can be ` ' or " CLI11_INLINE std::vector split_up(std::string str, char delimiter = '\0'); /// This function detects an equal or colon followed by an escaped quote after an argument /// then modifies the string to replace the equality with a space. This is needed /// to allow the split up function to work properly and is intended to be used with the find_and_modify function /// the return value is the offset+1 which is required by the find_and_modify function. CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset); /// Add quotes if the string contains spaces CLI11_INLINE std::string &add_quotes_if_needed(std::string &str); } // namespace detail namespace detail { CLI11_INLINE std::vector split(const std::string &s, char delim) { std::vector elems; // Check to see if empty string, give consistent result if(s.empty()) { elems.emplace_back(); } else { std::stringstream ss; ss.str(s); std::string item; while(std::getline(ss, item, delim)) { elems.push_back(item); } } return elems; } CLI11_INLINE std::string <rim(std::string &str) { auto it = std::find_if(str.begin(), str.end(), [](char ch) { return !std::isspace(ch, std::locale()); }); str.erase(str.begin(), it); return str; } CLI11_INLINE std::string <rim(std::string &str, const std::string &filter) { auto it = std::find_if(str.begin(), str.end(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); str.erase(str.begin(), it); return str; } CLI11_INLINE std::string &rtrim(std::string &str) { auto it = std::find_if(str.rbegin(), str.rend(), [](char ch) { return !std::isspace(ch, std::locale()); }); str.erase(it.base(), str.end()); return str; } CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter) { auto it = std::find_if(str.rbegin(), str.rend(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); str.erase(it.base(), str.end()); return str; } CLI11_INLINE std::string &remove_quotes(std::string &str) { if(str.length() > 1 && (str.front() == '"' || str.front() == '\'')) { if(str.front() == str.back()) { str.pop_back(); str.erase(str.begin(), str.begin() + 1); } } return str; } CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input) { std::string::size_type n = 0; while(n != std::string::npos && n < input.size()) { n = input.find('\n', n); if(n != std::string::npos) { input = input.substr(0, n + 1) + leader + input.substr(n + 1); n += leader.size(); } } return input; } CLI11_INLINE std::ostream & format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid) { name = " " + name; out << std::setw(static_cast(wid)) << std::left << name; if(!description.empty()) { if(name.length() >= wid) out << "\n" << std::setw(static_cast(wid)) << ""; for(const char c : description) { out.put(c); if(c == '\n') { out << std::setw(static_cast(wid)) << ""; } } } out << "\n"; return out; } CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid) { if(!aliases.empty()) { out << std::setw(static_cast(wid)) << " aliases: "; bool front = true; for(const auto &alias : aliases) { if(!front) { out << ", "; } else { front = false; } out << detail::fix_newlines(" ", alias); } out << "\n"; } return out; } CLI11_INLINE bool valid_name_string(const std::string &str) { if(str.empty() || !valid_first_char(str[0])) { return false; } auto e = str.end(); for(auto c = str.begin() + 1; c != e; ++c) if(!valid_later_char(*c)) return false; return true; } CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to) { std::size_t start_pos = 0; while((start_pos = str.find(from, start_pos)) != std::string::npos) { str.replace(start_pos, from.length(), to); start_pos += to.length(); } return str; } CLI11_INLINE void remove_default_flag_values(std::string &flags) { auto loc = flags.find_first_of('{', 2); while(loc != std::string::npos) { auto finish = flags.find_first_of("},", loc + 1); if((finish != std::string::npos) && (flags[finish] == '}')) { flags.erase(flags.begin() + static_cast(loc), flags.begin() + static_cast(finish) + 1); } loc = flags.find_first_of('{', loc + 1); } flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end()); } CLI11_INLINE std::ptrdiff_t find_member(std::string name, const std::vector names, bool ignore_case, bool ignore_underscore) { auto it = std::end(names); if(ignore_case) { if(ignore_underscore) { name = detail::to_lower(detail::remove_underscore(name)); it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { return detail::to_lower(detail::remove_underscore(local_name)) == name; }); } else { name = detail::to_lower(name); it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { return detail::to_lower(local_name) == name; }); } } else if(ignore_underscore) { name = detail::remove_underscore(name); it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { return detail::remove_underscore(local_name) == name; }); } else { it = std::find(std::begin(names), std::end(names), name); } return (it != std::end(names)) ? (it - std::begin(names)) : (-1); } CLI11_INLINE std::vector split_up(std::string str, char delimiter) { const std::string delims("\'\"`"); auto find_ws = [delimiter](char ch) { return (delimiter == '\0') ? std::isspace(ch, std::locale()) : (ch == delimiter); }; trim(str); std::vector output; bool embeddedQuote = false; char keyChar = ' '; while(!str.empty()) { if(delims.find_first_of(str[0]) != std::string::npos) { keyChar = str[0]; auto end = str.find_first_of(keyChar, 1); while((end != std::string::npos) && (str[end - 1] == '\\')) { // deal with escaped quotes end = str.find_first_of(keyChar, end + 1); embeddedQuote = true; } if(end != std::string::npos) { output.push_back(str.substr(1, end - 1)); if(end + 2 < str.size()) { str = str.substr(end + 2); } else { str.clear(); } } else { output.push_back(str.substr(1)); str = ""; } } else { auto it = std::find_if(std::begin(str), std::end(str), find_ws); if(it != std::end(str)) { std::string value = std::string(str.begin(), it); output.push_back(value); str = std::string(it + 1, str.end()); } else { output.push_back(str); str = ""; } } // transform any embedded quotes into the regular character if(embeddedQuote) { output.back() = find_and_replace(output.back(), std::string("\\") + keyChar, std::string(1, keyChar)); embeddedQuote = false; } trim(str); } return output; } CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset) { auto next = str[offset + 1]; if((next == '\"') || (next == '\'') || (next == '`')) { auto astart = str.find_last_of("-/ \"\'`", offset - 1); if(astart != std::string::npos) { if(str[astart] == ((str[offset] == '=') ? '-' : '/')) str[offset] = ' '; // interpret this as a space so the split_up works properly } } return offset + 1; } CLI11_INLINE std::string &add_quotes_if_needed(std::string &str) { if((str.front() != '"' && str.front() != '\'') || str.front() != str.back()) { char quote = str.find('"') < str.find('\'') ? '\'' : '"'; if(str.find(' ') != std::string::npos) { str.insert(0, 1, quote); str.append(1, quote); } } return str; } } // namespace detail // Use one of these on all error classes. // These are temporary and are undef'd at the end of this file. #define CLI11_ERROR_DEF(parent, name) \ protected: \ name(std::string ename, std::string msg, int exit_code) : parent(std::move(ename), std::move(msg), exit_code) {} \ name(std::string ename, std::string msg, ExitCodes exit_code) \ : parent(std::move(ename), std::move(msg), exit_code) {} \ \ public: \ name(std::string msg, ExitCodes exit_code) : parent(#name, std::move(msg), exit_code) {} \ name(std::string msg, int exit_code) : parent(#name, std::move(msg), exit_code) {} // This is added after the one above if a class is used directly and builds its own message #define CLI11_ERROR_SIMPLE(name) \ explicit name(std::string msg) : name(#name, msg, ExitCodes::name) {} /// These codes are part of every error in CLI. They can be obtained from e using e.exit_code or as a quick shortcut, /// int values from e.get_error_code(). enum class ExitCodes { Success = 0, IncorrectConstruction = 100, BadNameString, OptionAlreadyAdded, FileError, ConversionError, ValidationError, RequiredError, RequiresError, ExcludesError, ExtrasError, ConfigError, InvalidError, HorribleError, OptionNotFound, ArgumentMismatch, BaseClass = 127 }; // Error definitions /// @defgroup error_group Errors /// @brief Errors thrown by CLI11 /// /// These are the errors that can be thrown. Some of them, like CLI::Success, are not really errors. /// @{ /// All errors derive from this one class Error : public std::runtime_error { int actual_exit_code; std::string error_name{"Error"}; public: CLI11_NODISCARD int get_exit_code() const { return actual_exit_code; } CLI11_NODISCARD std::string get_name() const { return error_name; } Error(std::string name, std::string msg, int exit_code = static_cast(ExitCodes::BaseClass)) : runtime_error(msg), actual_exit_code(exit_code), error_name(std::move(name)) {} Error(std::string name, std::string msg, ExitCodes exit_code) : Error(name, msg, static_cast(exit_code)) {} }; // Note: Using Error::Error constructors does not work on GCC 4.7 /// Construction errors (not in parsing) class ConstructionError : public Error { CLI11_ERROR_DEF(Error, ConstructionError) }; /// Thrown when an option is set to conflicting values (non-vector and multi args, for example) class IncorrectConstruction : public ConstructionError { CLI11_ERROR_DEF(ConstructionError, IncorrectConstruction) CLI11_ERROR_SIMPLE(IncorrectConstruction) static IncorrectConstruction PositionalFlag(std::string name) { return IncorrectConstruction(name + ": Flags cannot be positional"); } static IncorrectConstruction Set0Opt(std::string name) { return IncorrectConstruction(name + ": Cannot set 0 expected, use a flag instead"); } static IncorrectConstruction SetFlag(std::string name) { return IncorrectConstruction(name + ": Cannot set an expected number for flags"); } static IncorrectConstruction ChangeNotVector(std::string name) { return IncorrectConstruction(name + ": You can only change the expected arguments for vectors"); } static IncorrectConstruction AfterMultiOpt(std::string name) { return IncorrectConstruction( name + ": You can't change expected arguments after you've changed the multi option policy!"); } static IncorrectConstruction MissingOption(std::string name) { return IncorrectConstruction("Option " + name + " is not defined"); } static IncorrectConstruction MultiOptionPolicy(std::string name) { return IncorrectConstruction(name + ": multi_option_policy only works for flags and exact value options"); } }; /// Thrown on construction of a bad name class BadNameString : public ConstructionError { CLI11_ERROR_DEF(ConstructionError, BadNameString) CLI11_ERROR_SIMPLE(BadNameString) static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); } static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); } static BadNameString DashesOnly(std::string name) { return BadNameString("Must have a name, not just dashes: " + name); } static BadNameString MultiPositionalNames(std::string name) { return BadNameString("Only one positional name allowed, remove: " + name); } }; /// Thrown when an option already exists class OptionAlreadyAdded : public ConstructionError { CLI11_ERROR_DEF(ConstructionError, OptionAlreadyAdded) explicit OptionAlreadyAdded(std::string name) : OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {} static OptionAlreadyAdded Requires(std::string name, std::string other) { return {name + " requires " + other, ExitCodes::OptionAlreadyAdded}; } static OptionAlreadyAdded Excludes(std::string name, std::string other) { return {name + " excludes " + other, ExitCodes::OptionAlreadyAdded}; } }; // Parsing errors /// Anything that can error in Parse class ParseError : public Error { CLI11_ERROR_DEF(Error, ParseError) }; // Not really "errors" /// This is a successful completion on parsing, supposed to exit class Success : public ParseError { CLI11_ERROR_DEF(ParseError, Success) Success() : Success("Successfully completed, should be caught and quit", ExitCodes::Success) {} }; /// -h or --help on command line class CallForHelp : public Success { CLI11_ERROR_DEF(Success, CallForHelp) CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} }; /// Usually something like --help-all on command line class CallForAllHelp : public Success { CLI11_ERROR_DEF(Success, CallForAllHelp) CallForAllHelp() : CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} }; /// -v or --version on command line class CallForVersion : public Success { CLI11_ERROR_DEF(Success, CallForVersion) CallForVersion() : CallForVersion("This should be caught in your main function, see examples", ExitCodes::Success) {} }; /// Does not output a diagnostic in CLI11_PARSE, but allows main() to return with a specific error code. class RuntimeError : public ParseError { CLI11_ERROR_DEF(ParseError, RuntimeError) explicit RuntimeError(int exit_code = 1) : RuntimeError("Runtime error", exit_code) {} }; /// Thrown when parsing an INI file and it is missing class FileError : public ParseError { CLI11_ERROR_DEF(ParseError, FileError) CLI11_ERROR_SIMPLE(FileError) static FileError Missing(std::string name) { return FileError(name + " was not readable (missing?)"); } }; /// Thrown when conversion call back fails, such as when an int fails to coerce to a string class ConversionError : public ParseError { CLI11_ERROR_DEF(ParseError, ConversionError) CLI11_ERROR_SIMPLE(ConversionError) ConversionError(std::string member, std::string name) : ConversionError("The value " + member + " is not an allowed value for " + name) {} ConversionError(std::string name, std::vector results) : ConversionError("Could not convert: " + name + " = " + detail::join(results)) {} static ConversionError TooManyInputsFlag(std::string name) { return ConversionError(name + ": too many inputs for a flag"); } static ConversionError TrueFalse(std::string name) { return ConversionError(name + ": Should be true/false or a number"); } }; /// Thrown when validation of results fails class ValidationError : public ParseError { CLI11_ERROR_DEF(ParseError, ValidationError) CLI11_ERROR_SIMPLE(ValidationError) explicit ValidationError(std::string name, std::string msg) : ValidationError(name + ": " + msg) {} }; /// Thrown when a required option is missing class RequiredError : public ParseError { CLI11_ERROR_DEF(ParseError, RequiredError) explicit RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {} static RequiredError Subcommand(std::size_t min_subcom) { if(min_subcom == 1) { return RequiredError("A subcommand"); } return {"Requires at least " + std::to_string(min_subcom) + " subcommands", ExitCodes::RequiredError}; } static RequiredError Option(std::size_t min_option, std::size_t max_option, std::size_t used, const std::string &option_list) { if((min_option == 1) && (max_option == 1) && (used == 0)) return RequiredError("Exactly 1 option from [" + option_list + "]"); if((min_option == 1) && (max_option == 1) && (used > 1)) { return {"Exactly 1 option from [" + option_list + "] is required and " + std::to_string(used) + " were given", ExitCodes::RequiredError}; } if((min_option == 1) && (used == 0)) return RequiredError("At least 1 option from [" + option_list + "]"); if(used < min_option) { return {"Requires at least " + std::to_string(min_option) + " options used and only " + std::to_string(used) + "were given from [" + option_list + "]", ExitCodes::RequiredError}; } if(max_option == 1) return {"Requires at most 1 options be given from [" + option_list + "]", ExitCodes::RequiredError}; return {"Requires at most " + std::to_string(max_option) + " options be used and " + std::to_string(used) + "were given from [" + option_list + "]", ExitCodes::RequiredError}; } }; /// Thrown when the wrong number of arguments has been received class ArgumentMismatch : public ParseError { CLI11_ERROR_DEF(ParseError, ArgumentMismatch) CLI11_ERROR_SIMPLE(ArgumentMismatch) ArgumentMismatch(std::string name, int expected, std::size_t received) : ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name + ", got " + std::to_string(received)) : ("Expected at least " + std::to_string(-expected) + " arguments to " + name + ", got " + std::to_string(received)), ExitCodes::ArgumentMismatch) {} static ArgumentMismatch AtLeast(std::string name, int num, std::size_t received) { return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required but received " + std::to_string(received)); } static ArgumentMismatch AtMost(std::string name, int num, std::size_t received) { return ArgumentMismatch(name + ": At Most " + std::to_string(num) + " required but received " + std::to_string(received)); } static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); } static ArgumentMismatch FlagOverride(std::string name) { return ArgumentMismatch(name + " was given a disallowed flag override"); } static ArgumentMismatch PartialType(std::string name, int num, std::string type) { return ArgumentMismatch(name + ": " + type + " only partially specified: " + std::to_string(num) + " required for each element"); } }; /// Thrown when a requires option is missing class RequiresError : public ParseError { CLI11_ERROR_DEF(ParseError, RequiresError) RequiresError(std::string curname, std::string subname) : RequiresError(curname + " requires " + subname, ExitCodes::RequiresError) {} }; /// Thrown when an excludes option is present class ExcludesError : public ParseError { CLI11_ERROR_DEF(ParseError, ExcludesError) ExcludesError(std::string curname, std::string subname) : ExcludesError(curname + " excludes " + subname, ExitCodes::ExcludesError) {} }; /// Thrown when too many positionals or options are found class ExtrasError : public ParseError { CLI11_ERROR_DEF(ParseError, ExtrasError) explicit ExtrasError(std::vector args) : ExtrasError((args.size() > 1 ? "The following arguments were not expected: " : "The following argument was not expected: ") + detail::rjoin(args, " "), ExitCodes::ExtrasError) {} ExtrasError(const std::string &name, std::vector args) : ExtrasError(name, (args.size() > 1 ? "The following arguments were not expected: " : "The following argument was not expected: ") + detail::rjoin(args, " "), ExitCodes::ExtrasError) {} }; /// Thrown when extra values are found in an INI file class ConfigError : public ParseError { CLI11_ERROR_DEF(ParseError, ConfigError) CLI11_ERROR_SIMPLE(ConfigError) static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); } static ConfigError NotConfigurable(std::string item) { return ConfigError(item + ": This option is not allowed in a configuration file"); } }; /// Thrown when validation fails before parsing class InvalidError : public ParseError { CLI11_ERROR_DEF(ParseError, InvalidError) explicit InvalidError(std::string name) : InvalidError(name + ": Too many positional arguments with unlimited expected args", ExitCodes::InvalidError) { } }; /// This is just a safety check to verify selection and parsing match - you should not ever see it /// Strings are directly added to this error, but again, it should never be seen. class HorribleError : public ParseError { CLI11_ERROR_DEF(ParseError, HorribleError) CLI11_ERROR_SIMPLE(HorribleError) }; // After parsing /// Thrown when counting a non-existent option class OptionNotFound : public Error { CLI11_ERROR_DEF(Error, OptionNotFound) explicit OptionNotFound(std::string name) : OptionNotFound(name + " not found", ExitCodes::OptionNotFound) {} }; #undef CLI11_ERROR_DEF #undef CLI11_ERROR_SIMPLE /// @} // Type tools // Utilities for type enabling namespace detail { // Based generally on https://rmf.io/cxx11/almost-static-if /// Simple empty scoped class enum class enabler {}; /// An instance to use in EnableIf constexpr enabler dummy = {}; } // namespace detail /// A copy of enable_if_t from C++14, compatible with C++11. /// /// We could check to see if C++14 is being used, but it does not hurt to redefine this /// (even Google does this: https://github.com/google/skia/blob/main/include/private/SkTLogic.h) /// It is not in the std namespace anyway, so no harm done. template using enable_if_t = typename std::enable_if::type; /// A copy of std::void_t from C++17 (helper for C++11 and C++14) template struct make_void { using type = void; }; /// A copy of std::void_t from C++17 - same reasoning as enable_if_t, it does not hurt to redefine template using void_t = typename make_void::type; /// A copy of std::conditional_t from C++14 - same reasoning as enable_if_t, it does not hurt to redefine template using conditional_t = typename std::conditional::type; /// Check to see if something is bool (fail check by default) template struct is_bool : std::false_type {}; /// Check to see if something is bool (true if actually a bool) template <> struct is_bool : std::true_type {}; /// Check to see if something is a shared pointer template struct is_shared_ptr : std::false_type {}; /// Check to see if something is a shared pointer (True if really a shared pointer) template struct is_shared_ptr> : std::true_type {}; /// Check to see if something is a shared pointer (True if really a shared pointer) template struct is_shared_ptr> : std::true_type {}; /// Check to see if something is copyable pointer template struct is_copyable_ptr { static bool const value = is_shared_ptr::value || std::is_pointer::value; }; /// This can be specialized to override the type deduction for IsMember. template struct IsMemberType { using type = T; }; /// The main custom type needed here is const char * should be a string. template <> struct IsMemberType { using type = std::string; }; namespace detail { // These are utilities for IsMember and other transforming objects /// Handy helper to access the element_type generically. This is not part of is_copyable_ptr because it requires that /// pointer_traits be valid. /// not a pointer template struct element_type { using type = T; }; template struct element_type::value>::type> { using type = typename std::pointer_traits::element_type; }; /// Combination of the element type and value type - remove pointer (including smart pointers) and get the value_type of /// the container template struct element_value_type { using type = typename element_type::type::value_type; }; /// Adaptor for set-like structure: This just wraps a normal container in a few utilities that do almost nothing. template struct pair_adaptor : std::false_type { using value_type = typename T::value_type; using first_type = typename std::remove_const::type; using second_type = typename std::remove_const::type; /// Get the first value (really just the underlying value) template static auto first(Q &&pair_value) -> decltype(std::forward(pair_value)) { return std::forward(pair_value); } /// Get the second value (really just the underlying value) template static auto second(Q &&pair_value) -> decltype(std::forward(pair_value)) { return std::forward(pair_value); } }; /// Adaptor for map-like structure (true version, must have key_type and mapped_type). /// This wraps a mapped container in a few utilities access it in a general way. template struct pair_adaptor< T, conditional_t, void>> : std::true_type { using value_type = typename T::value_type; using first_type = typename std::remove_const::type; using second_type = typename std::remove_const::type; /// Get the first value (really just the underlying value) template static auto first(Q &&pair_value) -> decltype(std::get<0>(std::forward(pair_value))) { return std::get<0>(std::forward(pair_value)); } /// Get the second value (really just the underlying value) template static auto second(Q &&pair_value) -> decltype(std::get<1>(std::forward(pair_value))) { return std::get<1>(std::forward(pair_value)); } }; // Warning is suppressed due to "bug" in gcc<5.0 and gcc 7.0 with c++17 enabled that generates a Wnarrowing warning // in the unevaluated context even if the function that was using this wasn't used. The standard says narrowing in // brace initialization shouldn't be allowed but for backwards compatibility gcc allows it in some contexts. It is a // little fuzzy what happens in template constructs and I think that was something GCC took a little while to work out. // But regardless some versions of gcc generate a warning when they shouldn't from the following code so that should be // suppressed #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnarrowing" #endif // check for constructibility from a specific type and copy assignable used in the parse detection template class is_direct_constructible { template static auto test(int, std::true_type) -> decltype( // NVCC warns about narrowing conversions here #ifdef __CUDACC__ #pragma diag_suppress 2361 #endif TT{std::declval()} #ifdef __CUDACC__ #pragma diag_default 2361 #endif , std::is_move_assignable()); template static auto test(int, std::false_type) -> std::false_type; template static auto test(...) -> std::false_type; public: static constexpr bool value = decltype(test(0, typename std::is_constructible::type()))::value; }; #ifdef __GNUC__ #pragma GCC diagnostic pop #endif // Check for output streamability // Based on https://stackoverflow.com/questions/22758291/how-can-i-detect-if-a-type-can-be-streamed-to-an-stdostream template class is_ostreamable { template static auto test(int) -> decltype(std::declval() << std::declval(), std::true_type()); template static auto test(...) -> std::false_type; public: static constexpr bool value = decltype(test(0))::value; }; /// Check for input streamability template class is_istreamable { template static auto test(int) -> decltype(std::declval() >> std::declval(), std::true_type()); template static auto test(...) -> std::false_type; public: static constexpr bool value = decltype(test(0))::value; }; /// Check for complex template class is_complex { template static auto test(int) -> decltype(std::declval().real(), std::declval().imag(), std::true_type()); template static auto test(...) -> std::false_type; public: static constexpr bool value = decltype(test(0))::value; }; /// Templated operation to get a value from a stream template ::value, detail::enabler> = detail::dummy> bool from_stream(const std::string &istring, T &obj) { std::istringstream is; is.str(istring); is >> obj; return !is.fail() && !is.rdbuf()->in_avail(); } template ::value, detail::enabler> = detail::dummy> bool from_stream(const std::string & /*istring*/, T & /*obj*/) { return false; } // check to see if an object is a mutable container (fail by default) template struct is_mutable_container : std::false_type {}; /// type trait to test if a type is a mutable container meaning it has a value_type, it has an iterator, a clear, and /// end methods and an insert function. And for our purposes we exclude std::string and types that can be constructed /// from a std::string template struct is_mutable_container< T, conditional_t().end()), decltype(std::declval().clear()), decltype(std::declval().insert(std::declval().end())>(), std::declval()))>, void>> : public conditional_t::value, std::false_type, std::true_type> {}; // check to see if an object is a mutable container (fail by default) template struct is_readable_container : std::false_type {}; /// type trait to test if a type is a container meaning it has a value_type, it has an iterator, a clear, and an end /// methods and an insert function. And for our purposes we exclude std::string and types that can be constructed from /// a std::string template struct is_readable_container< T, conditional_t().end()), decltype(std::declval().begin())>, void>> : public std::true_type {}; // check to see if an object is a wrapper (fail by default) template struct is_wrapper : std::false_type {}; // check if an object is a wrapper (it has a value_type defined) template struct is_wrapper, void>> : public std::true_type {}; // Check for tuple like types, as in classes with a tuple_size type trait template class is_tuple_like { template // static auto test(int) // -> decltype(std::conditional<(std::tuple_size::value > 0), std::true_type, std::false_type>::type()); static auto test(int) -> decltype(std::tuple_size::type>::value, std::true_type{}); template static auto test(...) -> std::false_type; public: static constexpr bool value = decltype(test(0))::value; }; /// Convert an object to a string (directly forward if this can become a string) template ::value, detail::enabler> = detail::dummy> auto to_string(T &&value) -> decltype(std::forward(value)) { return std::forward(value); } /// Construct a string from the object template ::value && !std::is_convertible::value, detail::enabler> = detail::dummy> std::string to_string(const T &value) { return std::string(value); // NOLINT(google-readability-casting) } /// Convert an object to a string (streaming must be supported for that type) template ::value && !std::is_constructible::value && is_ostreamable::value, detail::enabler> = detail::dummy> std::string to_string(T &&value) { std::stringstream stream; stream << value; return stream.str(); } /// If conversion is not supported, return an empty string (streaming is not supported for that type) template ::value && !is_ostreamable::value && !is_readable_container::type>::value, detail::enabler> = detail::dummy> std::string to_string(T &&) { return {}; } /// convert a readable container to a string template ::value && !is_ostreamable::value && is_readable_container::value, detail::enabler> = detail::dummy> std::string to_string(T &&variable) { auto cval = variable.begin(); auto end = variable.end(); if(cval == end) { return {"{}"}; } std::vector defaults; while(cval != end) { defaults.emplace_back(CLI::detail::to_string(*cval)); ++cval; } return {"[" + detail::join(defaults) + "]"}; } /// special template overload template ::value, detail::enabler> = detail::dummy> auto checked_to_string(T &&value) -> decltype(to_string(std::forward(value))) { return to_string(std::forward(value)); } /// special template overload template ::value, detail::enabler> = detail::dummy> std::string checked_to_string(T &&) { return std::string{}; } /// get a string as a convertible value for arithmetic types template ::value, detail::enabler> = detail::dummy> std::string value_string(const T &value) { return std::to_string(value); } /// get a string as a convertible value for enumerations template ::value, detail::enabler> = detail::dummy> std::string value_string(const T &value) { return std::to_string(static_cast::type>(value)); } /// for other types just use the regular to_string function template ::value && !std::is_arithmetic::value, detail::enabler> = detail::dummy> auto value_string(const T &value) -> decltype(to_string(value)) { return to_string(value); } /// template to get the underlying value type if it exists or use a default template struct wrapped_type { using type = def; }; /// Type size for regular object types that do not look like a tuple template struct wrapped_type::value>::type> { using type = typename T::value_type; }; /// This will only trigger for actual void type template struct type_count_base { static const int value{0}; }; /// Type size for regular object types that do not look like a tuple template struct type_count_base::value && !is_mutable_container::value && !std::is_void::value>::type> { static constexpr int value{1}; }; /// the base tuple size template struct type_count_base::value && !is_mutable_container::value>::type> { static constexpr int value{std::tuple_size::value}; }; /// Type count base for containers is the type_count_base of the individual element template struct type_count_base::value>::type> { static constexpr int value{type_count_base::value}; }; /// Set of overloads to get the type size of an object /// forward declare the subtype_count structure template struct subtype_count; /// forward declare the subtype_count_min structure template struct subtype_count_min; /// This will only trigger for actual void type template struct type_count { static const int value{0}; }; /// Type size for regular object types that do not look like a tuple template struct type_count::value && !is_tuple_like::value && !is_complex::value && !std::is_void::value>::type> { static constexpr int value{1}; }; /// Type size for complex since it sometimes looks like a wrapper template struct type_count::value>::type> { static constexpr int value{2}; }; /// Type size of types that are wrappers,except complex and tuples(which can also be wrappers sometimes) template struct type_count::value>::type> { static constexpr int value{subtype_count::value}; }; /// Type size of types that are wrappers,except containers complex and tuples(which can also be wrappers sometimes) template struct type_count::value && !is_complex::value && !is_tuple_like::value && !is_mutable_container::value>::type> { static constexpr int value{type_count::value}; }; /// 0 if the index > tuple size template constexpr typename std::enable_if::value, int>::type tuple_type_size() { return 0; } /// Recursively generate the tuple type name template constexpr typename std::enable_if < I::value, int>::type tuple_type_size() { return subtype_count::type>::value + tuple_type_size(); } /// Get the type size of the sum of type sizes for all the individual tuple types template struct type_count::value>::type> { static constexpr int value{tuple_type_size()}; }; /// definition of subtype count template struct subtype_count { static constexpr int value{is_mutable_container::value ? expected_max_vector_size : type_count::value}; }; /// This will only trigger for actual void type template struct type_count_min { static const int value{0}; }; /// Type size for regular object types that do not look like a tuple template struct type_count_min< T, typename std::enable_if::value && !is_tuple_like::value && !is_wrapper::value && !is_complex::value && !std::is_void::value>::type> { static constexpr int value{type_count::value}; }; /// Type size for complex since it sometimes looks like a wrapper template struct type_count_min::value>::type> { static constexpr int value{1}; }; /// Type size min of types that are wrappers,except complex and tuples(which can also be wrappers sometimes) template struct type_count_min< T, typename std::enable_if::value && !is_complex::value && !is_tuple_like::value>::type> { static constexpr int value{subtype_count_min::value}; }; /// 0 if the index > tuple size template constexpr typename std::enable_if::value, int>::type tuple_type_size_min() { return 0; } /// Recursively generate the tuple type name template constexpr typename std::enable_if < I::value, int>::type tuple_type_size_min() { return subtype_count_min::type>::value + tuple_type_size_min(); } /// Get the type size of the sum of type sizes for all the individual tuple types template struct type_count_min::value>::type> { static constexpr int value{tuple_type_size_min()}; }; /// definition of subtype count template struct subtype_count_min { static constexpr int value{is_mutable_container::value ? ((type_count::value < expected_max_vector_size) ? type_count::value : 0) : type_count_min::value}; }; /// This will only trigger for actual void type template struct expected_count { static const int value{0}; }; /// For most types the number of expected items is 1 template struct expected_count::value && !is_wrapper::value && !std::is_void::value>::type> { static constexpr int value{1}; }; /// number of expected items in a vector template struct expected_count::value>::type> { static constexpr int value{expected_max_vector_size}; }; /// number of expected items in a vector template struct expected_count::value && is_wrapper::value>::type> { static constexpr int value{expected_count::value}; }; // Enumeration of the different supported categorizations of objects enum class object_category : int { char_value = 1, integral_value = 2, unsigned_integral = 4, enumeration = 6, boolean_value = 8, floating_point = 10, number_constructible = 12, double_constructible = 14, integer_constructible = 16, // string like types string_assignable = 23, string_constructible = 24, other = 45, // special wrapper or container types wrapper_value = 50, complex_number = 60, tuple_value = 70, container_value = 80, }; /// Set of overloads to classify an object according to type /// some type that is not otherwise recognized template struct classify_object { static constexpr object_category value{object_category::other}; }; /// Signed integers template struct classify_object< T, typename std::enable_if::value && !std::is_same::value && std::is_signed::value && !is_bool::value && !std::is_enum::value>::type> { static constexpr object_category value{object_category::integral_value}; }; /// Unsigned integers template struct classify_object::value && std::is_unsigned::value && !std::is_same::value && !is_bool::value>::type> { static constexpr object_category value{object_category::unsigned_integral}; }; /// single character values template struct classify_object::value && !std::is_enum::value>::type> { static constexpr object_category value{object_category::char_value}; }; /// Boolean values template struct classify_object::value>::type> { static constexpr object_category value{object_category::boolean_value}; }; /// Floats template struct classify_object::value>::type> { static constexpr object_category value{object_category::floating_point}; }; /// String and similar direct assignment template struct classify_object::value && !std::is_integral::value && std::is_assignable::value>::type> { static constexpr object_category value{object_category::string_assignable}; }; /// String and similar constructible and copy assignment template struct classify_object< T, typename std::enable_if::value && !std::is_integral::value && !std::is_assignable::value && (type_count::value == 1) && std::is_constructible::value>::type> { static constexpr object_category value{object_category::string_constructible}; }; /// Enumerations template struct classify_object::value>::type> { static constexpr object_category value{object_category::enumeration}; }; template struct classify_object::value>::type> { static constexpr object_category value{object_category::complex_number}; }; /// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point, /// vectors, and enumerations template struct uncommon_type { using type = typename std::conditional::value && !std::is_integral::value && !std::is_assignable::value && !std::is_constructible::value && !is_complex::value && !is_mutable_container::value && !std::is_enum::value, std::true_type, std::false_type>::type; static constexpr bool value = type::value; }; /// wrapper type template struct classify_object::value && is_wrapper::value && !is_tuple_like::value && uncommon_type::value)>::type> { static constexpr object_category value{object_category::wrapper_value}; }; /// Assignable from double or int template struct classify_object::value && type_count::value == 1 && !is_wrapper::value && is_direct_constructible::value && is_direct_constructible::value>::type> { static constexpr object_category value{object_category::number_constructible}; }; /// Assignable from int template struct classify_object::value && type_count::value == 1 && !is_wrapper::value && !is_direct_constructible::value && is_direct_constructible::value>::type> { static constexpr object_category value{object_category::integer_constructible}; }; /// Assignable from double template struct classify_object::value && type_count::value == 1 && !is_wrapper::value && is_direct_constructible::value && !is_direct_constructible::value>::type> { static constexpr object_category value{object_category::double_constructible}; }; /// Tuple type template struct classify_object< T, typename std::enable_if::value && ((type_count::value >= 2 && !is_wrapper::value) || (uncommon_type::value && !is_direct_constructible::value && !is_direct_constructible::value) || (uncommon_type::value && type_count::value >= 2))>::type> { static constexpr object_category value{object_category::tuple_value}; // the condition on this class requires it be like a tuple, but on some compilers (like Xcode) tuples can be // constructed from just the first element so tuples of can be constructed from a string, which // could lead to issues so there are two variants of the condition, the first isolates things with a type size >=2 // mainly to get tuples on Xcode with the exception of wrappers, the second is the main one and just separating out // those cases that are caught by other object classifications }; /// container type template struct classify_object::value>::type> { static constexpr object_category value{object_category::container_value}; }; // Type name print /// Was going to be based on /// http://stackoverflow.com/questions/1055452/c-get-name-of-type-in-template /// But this is cleaner and works better in this case template ::value == object_category::char_value, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "CHAR"; } template ::value == object_category::integral_value || classify_object::value == object_category::integer_constructible, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "INT"; } template ::value == object_category::unsigned_integral, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "UINT"; } template ::value == object_category::floating_point || classify_object::value == object_category::number_constructible || classify_object::value == object_category::double_constructible, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "FLOAT"; } /// Print name for enumeration types template ::value == object_category::enumeration, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "ENUM"; } /// Print name for enumeration types template ::value == object_category::boolean_value, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "BOOLEAN"; } /// Print name for enumeration types template ::value == object_category::complex_number, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "COMPLEX"; } /// Print for all other types template ::value >= object_category::string_assignable && classify_object::value <= object_category::other, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "TEXT"; } /// typename for tuple value template ::value == object_category::tuple_value && type_count_base::value >= 2, detail::enabler> = detail::dummy> std::string type_name(); // forward declaration /// Generate type name for a wrapper or container value template ::value == object_category::container_value || classify_object::value == object_category::wrapper_value, detail::enabler> = detail::dummy> std::string type_name(); // forward declaration /// Print name for single element tuple types template ::value == object_category::tuple_value && type_count_base::value == 1, detail::enabler> = detail::dummy> inline std::string type_name() { return type_name::type>::type>(); } /// Empty string if the index > tuple size template inline typename std::enable_if::value, std::string>::type tuple_name() { return std::string{}; } /// Recursively generate the tuple type name template inline typename std::enable_if<(I < type_count_base::value), std::string>::type tuple_name() { auto str = std::string{type_name::type>::type>()} + ',' + tuple_name(); if(str.back() == ',') str.pop_back(); return str; } /// Print type name for tuples with 2 or more elements template ::value == object_category::tuple_value && type_count_base::value >= 2, detail::enabler>> inline std::string type_name() { auto tname = std::string(1, '[') + tuple_name(); tname.push_back(']'); return tname; } /// get the type name for a type that has a value_type member template ::value == object_category::container_value || classify_object::value == object_category::wrapper_value, detail::enabler>> inline std::string type_name() { return type_name(); } // Lexical cast /// Convert to an unsigned integral template ::value, detail::enabler> = detail::dummy> bool integral_conversion(const std::string &input, T &output) noexcept { if(input.empty() || input.front() == '-') { return false; } char *val = nullptr; errno = 0; std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0); if(errno == ERANGE) { return false; } output = static_cast(output_ll); if(val == (input.c_str() + input.size()) && static_cast(output) == output_ll) { return true; } val = nullptr; std::int64_t output_sll = std::strtoll(input.c_str(), &val, 0); if(val == (input.c_str() + input.size())) { output = (output_sll < 0) ? static_cast(0) : static_cast(output_sll); return (static_cast(output) == output_sll); } return false; } /// Convert to a signed integral template ::value, detail::enabler> = detail::dummy> bool integral_conversion(const std::string &input, T &output) noexcept { if(input.empty()) { return false; } char *val = nullptr; errno = 0; std::int64_t output_ll = std::strtoll(input.c_str(), &val, 0); if(errno == ERANGE) { return false; } output = static_cast(output_ll); if(val == (input.c_str() + input.size()) && static_cast(output) == output_ll) { return true; } if(input == "true") { // this is to deal with a few oddities with flags and wrapper int types output = static_cast(1); return true; } return false; } /// Convert a flag into an integer value typically binary flags inline std::int64_t to_flag_value(std::string val) { static const std::string trueString("true"); static const std::string falseString("false"); if(val == trueString) { return 1; } if(val == falseString) { return -1; } val = detail::to_lower(val); std::int64_t ret = 0; if(val.size() == 1) { if(val[0] >= '1' && val[0] <= '9') { return (static_cast(val[0]) - '0'); } switch(val[0]) { case '0': case 'f': case 'n': case '-': ret = -1; break; case 't': case 'y': case '+': ret = 1; break; default: throw std::invalid_argument("unrecognized character"); } return ret; } if(val == trueString || val == "on" || val == "yes" || val == "enable") { ret = 1; } else if(val == falseString || val == "off" || val == "no" || val == "disable") { ret = -1; } else { ret = std::stoll(val); } return ret; } /// Integer conversion template ::value == object_category::integral_value || classify_object::value == object_category::unsigned_integral, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { return integral_conversion(input, output); } /// char values template ::value == object_category::char_value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { if(input.size() == 1) { output = static_cast(input[0]); return true; } return integral_conversion(input, output); } /// Boolean values template ::value == object_category::boolean_value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { try { auto out = to_flag_value(input); output = (out > 0); return true; } catch(const std::invalid_argument &) { return false; } catch(const std::out_of_range &) { // if the number is out of the range of a 64 bit value then it is still a number and for this purpose is still // valid all we care about the sign output = (input[0] != '-'); return true; } } /// Floats template ::value == object_category::floating_point, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { if(input.empty()) { return false; } char *val = nullptr; auto output_ld = std::strtold(input.c_str(), &val); output = static_cast(output_ld); return val == (input.c_str() + input.size()); } /// complex template ::value == object_category::complex_number, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { using XC = typename wrapped_type::type; XC x{0.0}, y{0.0}; auto str1 = input; bool worked = false; auto nloc = str1.find_last_of("+-"); if(nloc != std::string::npos && nloc > 0) { worked = lexical_cast(str1.substr(0, nloc), x); str1 = str1.substr(nloc); if(str1.back() == 'i' || str1.back() == 'j') str1.pop_back(); worked = worked && lexical_cast(str1, y); } else { if(str1.back() == 'i' || str1.back() == 'j') { str1.pop_back(); worked = lexical_cast(str1, y); x = XC{0}; } else { worked = lexical_cast(str1, x); y = XC{0}; } } if(worked) { output = T{x, y}; return worked; } return from_stream(input, output); } /// String and similar direct assignment template ::value == object_category::string_assignable, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { output = input; return true; } /// String and similar constructible and copy assignment template < typename T, enable_if_t::value == object_category::string_constructible, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { output = T(input); return true; } /// Enumerations template ::value == object_category::enumeration, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { typename std::underlying_type::type val; if(!integral_conversion(input, val)) { return false; } output = static_cast(val); return true; } /// wrapper types template ::value == object_category::wrapper_value && std::is_assignable::value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { typename T::value_type val; if(lexical_cast(input, val)) { output = val; return true; } return from_stream(input, output); } template ::value == object_category::wrapper_value && !std::is_assignable::value && std::is_assignable::value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { typename T::value_type val; if(lexical_cast(input, val)) { output = T{val}; return true; } return from_stream(input, output); } /// Assignable from double or int template < typename T, enable_if_t::value == object_category::number_constructible, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { int val = 0; if(integral_conversion(input, val)) { output = T(val); return true; } double dval = 0.0; if(lexical_cast(input, dval)) { output = T{dval}; return true; } return from_stream(input, output); } /// Assignable from int template < typename T, enable_if_t::value == object_category::integer_constructible, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { int val = 0; if(integral_conversion(input, val)) { output = T(val); return true; } return from_stream(input, output); } /// Assignable from double template < typename T, enable_if_t::value == object_category::double_constructible, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { double val = 0.0; if(lexical_cast(input, val)) { output = T{val}; return true; } return from_stream(input, output); } /// Non-string convertible from an int template ::value == object_category::other && std::is_assignable::value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { int val = 0; if(integral_conversion(input, val)) { #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable : 4800) #endif // with Atomic this could produce a warning due to the conversion but if atomic gets here it is an old style // so will most likely still work output = val; #ifdef _MSC_VER #pragma warning(pop) #endif return true; } // LCOV_EXCL_START // This version of cast is only used for odd cases in an older compilers the fail over // from_stream is tested elsewhere an not relevant for coverage here return from_stream(input, output); // LCOV_EXCL_STOP } /// Non-string parsable by a stream template ::value == object_category::other && !std::is_assignable::value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { static_assert(is_istreamable::value, "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it " "is convertible from another type use the add_option(...) with XC being the known type"); return from_stream(input, output); } /// Assign a value through lexical cast operations /// Strings can be empty so we need to do a little different template ::value && (classify_object::value == object_category::string_assignable || classify_object::value == object_category::string_constructible), detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { return lexical_cast(input, output); } /// Assign a value through lexical cast operations template ::value && std::is_assignable::value && classify_object::value != object_category::string_assignable && classify_object::value != object_category::string_constructible, detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { if(input.empty()) { output = AssignTo{}; return true; } return lexical_cast(input, output); } /// Assign a value through lexical cast operations template ::value && !std::is_assignable::value && classify_object::value == object_category::wrapper_value, detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { if(input.empty()) { typename AssignTo::value_type emptyVal{}; output = emptyVal; return true; } return lexical_cast(input, output); } /// Assign a value through lexical cast operations for int compatible values /// mainly for atomic operations on some compilers template ::value && !std::is_assignable::value && classify_object::value != object_category::wrapper_value && std::is_assignable::value, detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { if(input.empty()) { output = 0; return true; } int val = 0; if(lexical_cast(input, val)) { output = val; return true; } return false; } /// Assign a value converted from a string in lexical cast to the output value directly template ::value && std::is_assignable::value, detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { ConvertTo val{}; bool parse_result = (!input.empty()) ? lexical_cast(input, val) : true; if(parse_result) { output = val; } return parse_result; } /// Assign a value from a lexical cast through constructing a value and move assigning it template < typename AssignTo, typename ConvertTo, enable_if_t::value && !std::is_assignable::value && std::is_move_assignable::value, detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, AssignTo &output) { ConvertTo val{}; bool parse_result = input.empty() ? true : lexical_cast(input, val); if(parse_result) { output = AssignTo(val); // use () form of constructor to allow some implicit conversions } return parse_result; } /// primary lexical conversion operation, 1 string to 1 type of some kind template ::value <= object_category::other && classify_object::value <= object_category::wrapper_value, detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { return lexical_assign(strings[0], output); } /// Lexical conversion if there is only one element but the conversion type is for two, then call a two element /// constructor template ::value <= 2) && expected_count::value == 1 && is_tuple_like::value && type_count_base::value == 2, detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { // the remove const is to handle pair types coming from a container typename std::remove_const::type>::type v1; typename std::tuple_element<1, ConvertTo>::type v2; bool retval = lexical_assign(strings[0], v1); if(strings.size() > 1) { retval = retval && lexical_assign(strings[1], v2); } if(retval) { output = AssignTo{v1, v2}; } return retval; } /// Lexical conversion of a container types of single elements template ::value && is_mutable_container::value && type_count::value == 1, detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { output.erase(output.begin(), output.end()); if(strings.size() == 1 && strings[0] == "{}") { return true; } bool skip_remaining = false; if(strings.size() == 2 && strings[0] == "{}" && is_separator(strings[1])) { skip_remaining = true; } for(const auto &elem : strings) { typename AssignTo::value_type out; bool retval = lexical_assign(elem, out); if(!retval) { return false; } output.insert(output.end(), std::move(out)); if(skip_remaining) { break; } } return (!output.empty()); } /// Lexical conversion for complex types template ::value, detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { if(strings.size() >= 2 && !strings[1].empty()) { using XC2 = typename wrapped_type::type; XC2 x{0.0}, y{0.0}; auto str1 = strings[1]; if(str1.back() == 'i' || str1.back() == 'j') { str1.pop_back(); } auto worked = lexical_cast(strings[0], x) && lexical_cast(str1, y); if(worked) { output = ConvertTo{x, y}; } return worked; } return lexical_assign(strings[0], output); } /// Conversion to a vector type using a particular single type as the conversion type template ::value && (expected_count::value == 1) && (type_count::value == 1), detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { bool retval = true; output.clear(); output.reserve(strings.size()); for(const auto &elem : strings) { output.emplace_back(); retval = retval && lexical_assign(elem, output.back()); } return (!output.empty()) && retval; } // forward declaration /// Lexical conversion of a container types with conversion type of two elements template ::value && is_mutable_container::value && type_count_base::value == 2, detail::enabler> = detail::dummy> bool lexical_conversion(std::vector strings, AssignTo &output); /// Lexical conversion of a vector types with type_size >2 forward declaration template ::value && is_mutable_container::value && type_count_base::value != 2 && ((type_count::value > 2) || (type_count::value > type_count_base::value)), detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output); /// Conversion for tuples template ::value && is_tuple_like::value && (type_count_base::value != type_count::value || type_count::value > 2), detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output); // forward declaration /// Conversion for operations where the assigned type is some class but the conversion is a mutable container or large /// tuple template ::value && !is_mutable_container::value && classify_object::value != object_category::wrapper_value && (is_mutable_container::value || type_count::value > 2), detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { if(strings.size() > 1 || (!strings.empty() && !(strings.front().empty()))) { ConvertTo val; auto retval = lexical_conversion(strings, val); output = AssignTo{val}; return retval; } output = AssignTo{}; return true; } /// function template for converting tuples if the static Index is greater than the tuple size template inline typename std::enable_if<(I >= type_count_base::value), bool>::type tuple_conversion(const std::vector &, AssignTo &) { return true; } /// Conversion of a tuple element where the type size ==1 and not a mutable container template inline typename std::enable_if::value && type_count::value == 1, bool>::type tuple_type_conversion(std::vector &strings, AssignTo &output) { auto retval = lexical_assign(strings[0], output); strings.erase(strings.begin()); return retval; } /// Conversion of a tuple element where the type size !=1 but the size is fixed and not a mutable container template inline typename std::enable_if::value && (type_count::value > 1) && type_count::value == type_count_min::value, bool>::type tuple_type_conversion(std::vector &strings, AssignTo &output) { auto retval = lexical_conversion(strings, output); strings.erase(strings.begin(), strings.begin() + type_count::value); return retval; } /// Conversion of a tuple element where the type is a mutable container or a type with different min and max type sizes template inline typename std::enable_if::value || type_count::value != type_count_min::value, bool>::type tuple_type_conversion(std::vector &strings, AssignTo &output) { std::size_t index{subtype_count_min::value}; const std::size_t mx_count{subtype_count::value}; const std::size_t mx{(std::max)(mx_count, strings.size())}; while(index < mx) { if(is_separator(strings[index])) { break; } ++index; } bool retval = lexical_conversion( std::vector(strings.begin(), strings.begin() + static_cast(index)), output); strings.erase(strings.begin(), strings.begin() + static_cast(index) + 1); return retval; } /// Tuple conversion operation template inline typename std::enable_if<(I < type_count_base::value), bool>::type tuple_conversion(std::vector strings, AssignTo &output) { bool retval = true; using ConvertToElement = typename std:: conditional::value, typename std::tuple_element::type, ConvertTo>::type; if(!strings.empty()) { retval = retval && tuple_type_conversion::type, ConvertToElement>( strings, std::get(output)); } retval = retval && tuple_conversion(std::move(strings), output); return retval; } /// Lexical conversion of a container types with tuple elements of size 2 template ::value && is_mutable_container::value && type_count_base::value == 2, detail::enabler>> bool lexical_conversion(std::vector strings, AssignTo &output) { output.clear(); while(!strings.empty()) { typename std::remove_const::type>::type v1; typename std::tuple_element<1, typename ConvertTo::value_type>::type v2; bool retval = tuple_type_conversion(strings, v1); if(!strings.empty()) { retval = retval && tuple_type_conversion(strings, v2); } if(retval) { output.insert(output.end(), typename AssignTo::value_type{v1, v2}); } else { return false; } } return (!output.empty()); } /// lexical conversion of tuples with type count>2 or tuples of types of some element with a type size>=2 template ::value && is_tuple_like::value && (type_count_base::value != type_count::value || type_count::value > 2), detail::enabler>> bool lexical_conversion(const std::vector &strings, AssignTo &output) { static_assert( !is_tuple_like::value || type_count_base::value == type_count_base::value, "if the conversion type is defined as a tuple it must be the same size as the type you are converting to"); return tuple_conversion(strings, output); } /// Lexical conversion of a vector types for everything but tuples of two elements and types of size 1 template ::value && is_mutable_container::value && type_count_base::value != 2 && ((type_count::value > 2) || (type_count::value > type_count_base::value)), detail::enabler>> bool lexical_conversion(const std::vector &strings, AssignTo &output) { bool retval = true; output.clear(); std::vector temp; std::size_t ii{0}; std::size_t icount{0}; std::size_t xcm{type_count::value}; auto ii_max = strings.size(); while(ii < ii_max) { temp.push_back(strings[ii]); ++ii; ++icount; if(icount == xcm || is_separator(temp.back()) || ii == ii_max) { if(static_cast(xcm) > type_count_min::value && is_separator(temp.back())) { temp.pop_back(); } typename AssignTo::value_type temp_out; retval = retval && lexical_conversion(temp, temp_out); temp.clear(); if(!retval) { return false; } output.insert(output.end(), std::move(temp_out)); icount = 0; } } return retval; } /// conversion for wrapper types template ::value == object_category::wrapper_value && std::is_assignable::value, detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { if(strings.empty() || strings.front().empty()) { output = ConvertTo{}; return true; } typename ConvertTo::value_type val; if(lexical_conversion(strings, val)) { output = ConvertTo{val}; return true; } return false; } /// conversion for wrapper types template ::value == object_category::wrapper_value && !std::is_assignable::value, detail::enabler> = detail::dummy> bool lexical_conversion(const std::vector &strings, AssignTo &output) { using ConvertType = typename ConvertTo::value_type; if(strings.empty() || strings.front().empty()) { output = ConvertType{}; return true; } ConvertType val; if(lexical_conversion(strings, val)) { output = val; return true; } return false; } /// Sum a vector of strings inline std::string sum_string_vector(const std::vector &values) { double val{0.0}; bool fail{false}; std::string output; for(const auto &arg : values) { double tv{0.0}; auto comp = lexical_cast(arg, tv); if(!comp) { try { tv = static_cast(detail::to_flag_value(arg)); } catch(const std::exception &) { fail = true; break; } } val += tv; } if(fail) { for(const auto &arg : values) { output.append(arg); } } else { if(val <= static_cast((std::numeric_limits::min)()) || val >= static_cast((std::numeric_limits::max)()) || std::ceil(val) == std::floor(val)) { output = detail::value_string(static_cast(val)); } else { output = detail::value_string(val); } } return output; } } // namespace detail namespace detail { // Returns false if not a short option. Otherwise, sets opt name and rest and returns true CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std::string &rest); // Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value); // Returns false if not a windows style option. Otherwise, sets opt name and value and returns true CLI11_INLINE bool split_windows_style(const std::string ¤t, std::string &name, std::string &value); // Splits a string into multiple long and short names CLI11_INLINE std::vector split_names(std::string current); /// extract default flag values either {def} or starting with a ! CLI11_INLINE std::vector> get_default_flag_values(const std::string &str); /// Get a vector of short names, one of long names, and a single name CLI11_INLINE std::tuple, std::vector, std::string> get_names(const std::vector &input); } // namespace detail namespace detail { CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std::string &rest) { if(current.size() > 1 && current[0] == '-' && valid_first_char(current[1])) { name = current.substr(1, 1); rest = current.substr(2); return true; } return false; } CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value) { if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) { auto loc = current.find_first_of('='); if(loc != std::string::npos) { name = current.substr(2, loc - 2); value = current.substr(loc + 1); } else { name = current.substr(2); value = ""; } return true; } return false; } CLI11_INLINE bool split_windows_style(const std::string ¤t, std::string &name, std::string &value) { if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) { auto loc = current.find_first_of(':'); if(loc != std::string::npos) { name = current.substr(1, loc - 1); value = current.substr(loc + 1); } else { name = current.substr(1); value = ""; } return true; } return false; } CLI11_INLINE std::vector split_names(std::string current) { std::vector output; std::size_t val = 0; while((val = current.find(',')) != std::string::npos) { output.push_back(trim_copy(current.substr(0, val))); current = current.substr(val + 1); } output.push_back(trim_copy(current)); return output; } CLI11_INLINE std::vector> get_default_flag_values(const std::string &str) { std::vector flags = split_names(str); flags.erase(std::remove_if(flags.begin(), flags.end(), [](const std::string &name) { return ((name.empty()) || (!(((name.find_first_of('{') != std::string::npos) && (name.back() == '}')) || (name[0] == '!')))); }), flags.end()); std::vector> output; output.reserve(flags.size()); for(auto &flag : flags) { auto def_start = flag.find_first_of('{'); std::string defval = "false"; if((def_start != std::string::npos) && (flag.back() == '}')) { defval = flag.substr(def_start + 1); defval.pop_back(); flag.erase(def_start, std::string::npos); // NOLINT(readability-suspicious-call-argument) } flag.erase(0, flag.find_first_not_of("-!")); output.emplace_back(flag, defval); } return output; } CLI11_INLINE std::tuple, std::vector, std::string> get_names(const std::vector &input) { std::vector short_names; std::vector long_names; std::string pos_name; for(std::string name : input) { if(name.length() == 0) { continue; } if(name.length() > 1 && name[0] == '-' && name[1] != '-') { if(name.length() == 2 && valid_first_char(name[1])) short_names.emplace_back(1, name[1]); else throw BadNameString::OneCharName(name); } else if(name.length() > 2 && name.substr(0, 2) == "--") { name = name.substr(2); if(valid_name_string(name)) long_names.push_back(name); else throw BadNameString::BadLongName(name); } else if(name == "-" || name == "--") { throw BadNameString::DashesOnly(name); } else { if(pos_name.length() > 0) throw BadNameString::MultiPositionalNames(name); pos_name = name; } } return std::make_tuple(short_names, long_names, pos_name); } } // namespace detail class App; /// Holds values to load into Options struct ConfigItem { /// This is the list of parents std::vector parents{}; /// This is the name std::string name{}; /// Listing of inputs std::vector inputs{}; /// The list of parents and name joined by "." CLI11_NODISCARD std::string fullname() const { std::vector tmp = parents; tmp.emplace_back(name); return detail::join(tmp, "."); } }; /// This class provides a converter for configuration files. class Config { protected: std::vector items{}; public: /// Convert an app into a configuration virtual std::string to_config(const App *, bool, bool, std::string) const = 0; /// Convert a configuration into an app virtual std::vector from_config(std::istream &) const = 0; /// Get a flag value CLI11_NODISCARD virtual std::string to_flag(const ConfigItem &item) const { if(item.inputs.size() == 1) { return item.inputs.at(0); } if(item.inputs.empty()) { return "{}"; } throw ConversionError::TooManyInputsFlag(item.fullname()); // LCOV_EXCL_LINE } /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure CLI11_NODISCARD std::vector from_file(const std::string &name) const { std::ifstream input{name}; if(!input.good()) throw FileError::Missing(name); return from_config(input); } /// Virtual destructor virtual ~Config() = default; }; /// This converter works with INI/TOML files; to write INI files use ConfigINI class ConfigBase : public Config { protected: /// the character used for comments char commentChar = '#'; /// the character used to start an array '\0' is a default to not use char arrayStart = '['; /// the character used to end an array '\0' is a default to not use char arrayEnd = ']'; /// the character used to separate elements in an array char arraySeparator = ','; /// the character used separate the name from the value char valueDelimiter = '='; /// the character to use around strings char stringQuote = '"'; /// the character to use around single characters char characterQuote = '\''; /// the maximum number of layers to allow uint8_t maximumLayers{255}; /// the separator used to separator parent layers char parentSeparatorChar{'.'}; /// Specify the configuration index to use for arrayed sections int16_t configIndex{-1}; /// Specify the configuration section that should be used std::string configSection{}; public: std::string to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override; std::vector from_config(std::istream &input) const override; /// Specify the configuration for comment characters ConfigBase *comment(char cchar) { commentChar = cchar; return this; } /// Specify the start and end characters for an array ConfigBase *arrayBounds(char aStart, char aEnd) { arrayStart = aStart; arrayEnd = aEnd; return this; } /// Specify the delimiter character for an array ConfigBase *arrayDelimiter(char aSep) { arraySeparator = aSep; return this; } /// Specify the delimiter between a name and value ConfigBase *valueSeparator(char vSep) { valueDelimiter = vSep; return this; } /// Specify the quote characters used around strings and characters ConfigBase *quoteCharacter(char qString, char qChar) { stringQuote = qString; characterQuote = qChar; return this; } /// Specify the maximum number of parents ConfigBase *maxLayers(uint8_t layers) { maximumLayers = layers; return this; } /// Specify the separator to use for parent layers ConfigBase *parentSeparator(char sep) { parentSeparatorChar = sep; return this; } /// get a reference to the configuration section std::string §ionRef() { return configSection; } /// get the section CLI11_NODISCARD const std::string §ion() const { return configSection; } /// specify a particular section of the configuration file to use ConfigBase *section(const std::string §ionName) { configSection = sectionName; return this; } /// get a reference to the configuration index int16_t &indexRef() { return configIndex; } /// get the section index CLI11_NODISCARD int16_t index() const { return configIndex; } /// specify a particular index in the section to use (-1) for all sections to use ConfigBase *index(int16_t sectionIndex) { configIndex = sectionIndex; return this; } }; /// the default Config is the TOML file format using ConfigTOML = ConfigBase; /// ConfigINI generates a "standard" INI compliant output class ConfigINI : public ConfigTOML { public: ConfigINI() { commentChar = ';'; arrayStart = '\0'; arrayEnd = '\0'; arraySeparator = ' '; valueDelimiter = '='; } }; class Option; /// @defgroup validator_group Validators /// @brief Some validators that are provided /// /// These are simple `std::string(const std::string&)` validators that are useful. They return /// a string if the validation fails. A custom struct is provided, as well, with the same user /// semantics, but with the ability to provide a new type name. /// @{ /// class Validator { protected: /// This is the description function, if empty the description_ will be used std::function desc_function_{[]() { return std::string{}; }}; /// This is the base function that is to be called. /// Returns a string error message if validation fails. std::function func_{[](std::string &) { return std::string{}; }}; /// The name for search purposes of the Validator std::string name_{}; /// A Validator will only apply to an indexed value (-1 is all elements) int application_index_ = -1; /// Enable for Validator to allow it to be disabled if need be bool active_{true}; /// specify that a validator should not modify the input bool non_modifying_{false}; Validator(std::string validator_desc, std::function func) : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(func)) {} public: Validator() = default; /// Construct a Validator with just the description string explicit Validator(std::string validator_desc) : desc_function_([validator_desc]() { return validator_desc; }) {} /// Construct Validator from basic information Validator(std::function op, std::string validator_desc, std::string validator_name = "") : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(op)), name_(std::move(validator_name)) {} /// Set the Validator operation function Validator &operation(std::function op) { func_ = std::move(op); return *this; } /// This is the required operator for a Validator - provided to help /// users (CLI11 uses the member `func` directly) std::string operator()(std::string &str) const; /// This is the required operator for a Validator - provided to help /// users (CLI11 uses the member `func` directly) std::string operator()(const std::string &str) const { std::string value = str; return (active_) ? func_(value) : std::string{}; } /// Specify the type string Validator &description(std::string validator_desc) { desc_function_ = [validator_desc]() { return validator_desc; }; return *this; } /// Specify the type string CLI11_NODISCARD Validator description(std::string validator_desc) const; /// Generate type description information for the Validator CLI11_NODISCARD std::string get_description() const { if(active_) { return desc_function_(); } return std::string{}; } /// Specify the type string Validator &name(std::string validator_name) { name_ = std::move(validator_name); return *this; } /// Specify the type string CLI11_NODISCARD Validator name(std::string validator_name) const { Validator newval(*this); newval.name_ = std::move(validator_name); return newval; } /// Get the name of the Validator CLI11_NODISCARD const std::string &get_name() const { return name_; } /// Specify whether the Validator is active or not Validator &active(bool active_val = true) { active_ = active_val; return *this; } /// Specify whether the Validator is active or not CLI11_NODISCARD Validator active(bool active_val = true) const { Validator newval(*this); newval.active_ = active_val; return newval; } /// Specify whether the Validator can be modifying or not Validator &non_modifying(bool no_modify = true) { non_modifying_ = no_modify; return *this; } /// Specify the application index of a validator Validator &application_index(int app_index) { application_index_ = app_index; return *this; } /// Specify the application index of a validator CLI11_NODISCARD Validator application_index(int app_index) const { Validator newval(*this); newval.application_index_ = app_index; return newval; } /// Get the current value of the application index CLI11_NODISCARD int get_application_index() const { return application_index_; } /// Get a boolean if the validator is active CLI11_NODISCARD bool get_active() const { return active_; } /// Get a boolean if the validator is allowed to modify the input returns true if it can modify the input CLI11_NODISCARD bool get_modifying() const { return !non_modifying_; } /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the /// same. Validator operator&(const Validator &other) const; /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the /// same. Validator operator|(const Validator &other) const; /// Create a validator that fails when a given validator succeeds Validator operator!() const; private: void _merge_description(const Validator &val1, const Validator &val2, const std::string &merger); }; /// Class wrapping some of the accessors of Validator class CustomValidator : public Validator { public: }; // The implementation of the built in validators is using the Validator class; // the user is only expected to use the const (static) versions (since there's no setup). // Therefore, this is in detail. namespace detail { /// CLI enumeration of different file types enum class path_type { nonexistent, file, directory }; /// get the type of the path from a file name CLI11_INLINE path_type check_path(const char *file) noexcept; /// Check for an existing file (returns error message if check fails) class ExistingFileValidator : public Validator { public: ExistingFileValidator(); }; /// Check for an existing directory (returns error message if check fails) class ExistingDirectoryValidator : public Validator { public: ExistingDirectoryValidator(); }; /// Check for an existing path class ExistingPathValidator : public Validator { public: ExistingPathValidator(); }; /// Check for an non-existing path class NonexistentPathValidator : public Validator { public: NonexistentPathValidator(); }; /// Validate the given string is a legal ipv4 address class IPV4Validator : public Validator { public: IPV4Validator(); }; } // namespace detail // Static is not needed here, because global const implies static. /// Check for existing file (returns error message if check fails) const detail::ExistingFileValidator ExistingFile; /// Check for an existing directory (returns error message if check fails) const detail::ExistingDirectoryValidator ExistingDirectory; /// Check for an existing path const detail::ExistingPathValidator ExistingPath; /// Check for an non-existing path const detail::NonexistentPathValidator NonexistentPath; /// Check for an IP4 address const detail::IPV4Validator ValidIPV4; /// Validate the input as a particular type template class TypeValidator : public Validator { public: explicit TypeValidator(const std::string &validator_name) : Validator(validator_name, [](std::string &input_string) { using CLI::detail::lexical_cast; auto val = DesiredType(); if(!lexical_cast(input_string, val)) { return std::string("Failed parsing ") + input_string + " as a " + detail::type_name(); } return std::string(); }) {} TypeValidator() : TypeValidator(detail::type_name()) {} }; /// Check for a number const TypeValidator Number("NUMBER"); /// Modify a path if the file is a particular default location, can be used as Check or transform /// with the error return optionally disabled class FileOnDefaultPath : public Validator { public: explicit FileOnDefaultPath(std::string default_path, bool enableErrorReturn = true); }; /// Produce a range (factory). Min and max are inclusive. class Range : public Validator { public: /// This produces a range with min and max inclusive. /// /// Note that the constructor is templated, but the struct is not, so C++17 is not /// needed to provide nice syntax for Range(a,b). template Range(T min_val, T max_val, const std::string &validator_name = std::string{}) : Validator(validator_name) { if(validator_name.empty()) { std::stringstream out; out << detail::type_name() << " in [" << min_val << " - " << max_val << "]"; description(out.str()); } func_ = [min_val, max_val](std::string &input) { using CLI::detail::lexical_cast; T val; bool converted = lexical_cast(input, val); if((!converted) || (val < min_val || val > max_val)) { std::stringstream out; out << "Value " << input << " not in range ["; out << min_val << " - " << max_val << "]"; return out.str(); } return std::string{}; }; } /// Range of one value is 0 to value template explicit Range(T max_val, const std::string &validator_name = std::string{}) : Range(static_cast(0), max_val, validator_name) {} }; /// Check for a non negative number const Range NonNegativeNumber((std::numeric_limits::max)(), "NONNEGATIVE"); /// Check for a positive valued number (val>0.0), ::min here is the smallest positive number const Range PositiveNumber((std::numeric_limits::min)(), (std::numeric_limits::max)(), "POSITIVE"); /// Produce a bounded range (factory). Min and max are inclusive. class Bound : public Validator { public: /// This bounds a value with min and max inclusive. /// /// Note that the constructor is templated, but the struct is not, so C++17 is not /// needed to provide nice syntax for Range(a,b). template Bound(T min_val, T max_val) { std::stringstream out; out << detail::type_name() << " bounded to [" << min_val << " - " << max_val << "]"; description(out.str()); func_ = [min_val, max_val](std::string &input) { using CLI::detail::lexical_cast; T val; bool converted = lexical_cast(input, val); if(!converted) { return std::string("Value ") + input + " could not be converted"; } if(val < min_val) input = detail::to_string(min_val); else if(val > max_val) input = detail::to_string(max_val); return std::string{}; }; } /// Range of one value is 0 to value template explicit Bound(T max_val) : Bound(static_cast(0), max_val) {} }; namespace detail { template ::type>::value, detail::enabler> = detail::dummy> auto smart_deref(T value) -> decltype(*value) { return *value; } template < typename T, enable_if_t::type>::value, detail::enabler> = detail::dummy> typename std::remove_reference::type &smart_deref(T &value) { return value; } /// Generate a string representation of a set template std::string generate_set(const T &set) { using element_t = typename detail::element_type::type; using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair std::string out(1, '{'); out.append(detail::join( detail::smart_deref(set), [](const iteration_type_t &v) { return detail::pair_adaptor::first(v); }, ",")); out.push_back('}'); return out; } /// Generate a string representation of a map template std::string generate_map(const T &map, bool key_only = false) { using element_t = typename detail::element_type::type; using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair std::string out(1, '{'); out.append(detail::join( detail::smart_deref(map), [key_only](const iteration_type_t &v) { std::string res{detail::to_string(detail::pair_adaptor::first(v))}; if(!key_only) { res.append("->"); res += detail::to_string(detail::pair_adaptor::second(v)); } return res; }, ",")); out.push_back('}'); return out; } template struct has_find { template static auto test(int) -> decltype(std::declval().find(std::declval()), std::true_type()); template static auto test(...) -> decltype(std::false_type()); static const auto value = decltype(test(0))::value; using type = std::integral_constant; }; /// A search function template ::value, detail::enabler> = detail::dummy> auto search(const T &set, const V &val) -> std::pair { using element_t = typename detail::element_type::type; auto &setref = detail::smart_deref(set); auto it = std::find_if(std::begin(setref), std::end(setref), [&val](decltype(*std::begin(setref)) v) { return (detail::pair_adaptor::first(v) == val); }); return {(it != std::end(setref)), it}; } /// A search function that uses the built in find function template ::value, detail::enabler> = detail::dummy> auto search(const T &set, const V &val) -> std::pair { auto &setref = detail::smart_deref(set); auto it = setref.find(val); return {(it != std::end(setref)), it}; } /// A search function with a filter function template auto search(const T &set, const V &val, const std::function &filter_function) -> std::pair { using element_t = typename detail::element_type::type; // do the potentially faster first search auto res = search(set, val); if((res.first) || (!(filter_function))) { return res; } // if we haven't found it do the longer linear search with all the element translations auto &setref = detail::smart_deref(set); auto it = std::find_if(std::begin(setref), std::end(setref), [&](decltype(*std::begin(setref)) v) { V a{detail::pair_adaptor::first(v)}; a = filter_function(a); return (a == val); }); return {(it != std::end(setref)), it}; } // the following suggestion was made by Nikita Ofitserov(@himikof) // done in templates to prevent compiler warnings on negation of unsigned numbers /// Do a check for overflow on signed numbers template inline typename std::enable_if::value, T>::type overflowCheck(const T &a, const T &b) { if((a > 0) == (b > 0)) { return ((std::numeric_limits::max)() / (std::abs)(a) < (std::abs)(b)); } return ((std::numeric_limits::min)() / (std::abs)(a) > -(std::abs)(b)); } /// Do a check for overflow on unsigned numbers template inline typename std::enable_if::value, T>::type overflowCheck(const T &a, const T &b) { return ((std::numeric_limits::max)() / a < b); } /// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise. template typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { if(a == 0 || b == 0 || a == 1 || b == 1) { a *= b; return true; } if(a == (std::numeric_limits::min)() || b == (std::numeric_limits::min)()) { return false; } if(overflowCheck(a, b)) { return false; } a *= b; return true; } /// Performs a *= b; if it doesn't equal infinity. Returns false otherwise. template typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { T c = a * b; if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) { return false; } a = c; return true; } } // namespace detail /// Verify items are in a set class IsMember : public Validator { public: using filter_fn_t = std::function; /// This allows in-place construction using an initializer list template IsMember(std::initializer_list values, Args &&...args) : IsMember(std::vector(values), std::forward(args)...) {} /// This checks to see if an item is in a set (empty function) template explicit IsMember(T &&set) : IsMember(std::forward(set), nullptr) {} /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter /// both sides of the comparison before computing the comparison. template explicit IsMember(T set, F filter_function) { // Get the type of the contained item - requires a container have ::value_type // if the type does not have first_type and second_type, these are both value_type using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map using local_item_t = typename IsMemberType::type; // This will convert bad types to good ones // (const char * to std::string) // Make a local copy of the filter function, using a std::function if not one already std::function filter_fn = filter_function; // This is the type name for help, it will take the current version of the set contents desc_function_ = [set]() { return detail::generate_set(detail::smart_deref(set)); }; // This is the function that validates // It stores a copy of the set pointer-like, so shared_ptr will stay alive func_ = [set, filter_fn](std::string &input) { using CLI::detail::lexical_cast; local_item_t b; if(!lexical_cast(input, b)) { throw ValidationError(input); // name is added later } if(filter_fn) { b = filter_fn(b); } auto res = detail::search(set, b, filter_fn); if(res.first) { // Make sure the version in the input string is identical to the one in the set if(filter_fn) { input = detail::value_string(detail::pair_adaptor::first(*(res.second))); } // Return empty error string (success) return std::string{}; } // If you reach this point, the result was not found return input + " not in " + detail::generate_set(detail::smart_deref(set)); }; } /// You can pass in as many filter functions as you like, they nest (string only currently) template IsMember(T &&set, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) : IsMember( std::forward(set), [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, other...) {} }; /// definition of the default transformation object template using TransformPairs = std::vector>; /// Translate named items to other or a value set class Transformer : public Validator { public: using filter_fn_t = std::function; /// This allows in-place construction template Transformer(std::initializer_list> values, Args &&...args) : Transformer(TransformPairs(values), std::forward(args)...) {} /// direct map of std::string to std::string template explicit Transformer(T &&mapping) : Transformer(std::forward(mapping), nullptr) {} /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter /// both sides of the comparison before computing the comparison. template explicit Transformer(T mapping, F filter_function) { static_assert(detail::pair_adaptor::type>::value, "mapping must produce value pairs"); // Get the type of the contained item - requires a container have ::value_type // if the type does not have first_type and second_type, these are both value_type using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map using local_item_t = typename IsMemberType::type; // Will convert bad types to good ones // (const char * to std::string) // Make a local copy of the filter function, using a std::function if not one already std::function filter_fn = filter_function; // This is the type name for help, it will take the current version of the set contents desc_function_ = [mapping]() { return detail::generate_map(detail::smart_deref(mapping)); }; func_ = [mapping, filter_fn](std::string &input) { using CLI::detail::lexical_cast; local_item_t b; if(!lexical_cast(input, b)) { return std::string(); // there is no possible way we can match anything in the mapping if we can't convert so just return } if(filter_fn) { b = filter_fn(b); } auto res = detail::search(mapping, b, filter_fn); if(res.first) { input = detail::value_string(detail::pair_adaptor::second(*res.second)); } return std::string{}; }; } /// You can pass in as many filter functions as you like, they nest template Transformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) : Transformer( std::forward(mapping), [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, other...) {} }; /// translate named items to other or a value set class CheckedTransformer : public Validator { public: using filter_fn_t = std::function; /// This allows in-place construction template CheckedTransformer(std::initializer_list> values, Args &&...args) : CheckedTransformer(TransformPairs(values), std::forward(args)...) {} /// direct map of std::string to std::string template explicit CheckedTransformer(T mapping) : CheckedTransformer(std::move(mapping), nullptr) {} /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter /// both sides of the comparison before computing the comparison. template explicit CheckedTransformer(T mapping, F filter_function) { static_assert(detail::pair_adaptor::type>::value, "mapping must produce value pairs"); // Get the type of the contained item - requires a container have ::value_type // if the type does not have first_type and second_type, these are both value_type using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map using local_item_t = typename IsMemberType::type; // Will convert bad types to good ones // (const char * to std::string) using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair // Make a local copy of the filter function, using a std::function if not one already std::function filter_fn = filter_function; auto tfunc = [mapping]() { std::string out("value in "); out += detail::generate_map(detail::smart_deref(mapping)) + " OR {"; out += detail::join( detail::smart_deref(mapping), [](const iteration_type_t &v) { return detail::to_string(detail::pair_adaptor::second(v)); }, ","); out.push_back('}'); return out; }; desc_function_ = tfunc; func_ = [mapping, tfunc, filter_fn](std::string &input) { using CLI::detail::lexical_cast; local_item_t b; bool converted = lexical_cast(input, b); if(converted) { if(filter_fn) { b = filter_fn(b); } auto res = detail::search(mapping, b, filter_fn); if(res.first) { input = detail::value_string(detail::pair_adaptor::second(*res.second)); return std::string{}; } } for(const auto &v : detail::smart_deref(mapping)) { auto output_string = detail::value_string(detail::pair_adaptor::second(v)); if(output_string == input) { return std::string(); } } return "Check " + input + " " + tfunc() + " FAILED"; }; } /// You can pass in as many filter functions as you like, they nest template CheckedTransformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) : CheckedTransformer( std::forward(mapping), [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, other...) {} }; /// Helper function to allow ignore_case to be passed to IsMember or Transform inline std::string ignore_case(std::string item) { return detail::to_lower(item); } /// Helper function to allow ignore_underscore to be passed to IsMember or Transform inline std::string ignore_underscore(std::string item) { return detail::remove_underscore(item); } /// Helper function to allow checks to ignore spaces to be passed to IsMember or Transform inline std::string ignore_space(std::string item) { item.erase(std::remove(std::begin(item), std::end(item), ' '), std::end(item)); item.erase(std::remove(std::begin(item), std::end(item), '\t'), std::end(item)); return item; } /// Multiply a number by a factor using given mapping. /// Can be used to write transforms for SIZE or DURATION inputs. /// /// Example: /// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}` /// one can recognize inputs like "100", "12kb", "100 MB", /// that will be automatically transformed to 100, 14448, 104857600. /// /// Output number type matches the type in the provided mapping. /// Therefore, if it is required to interpret real inputs like "0.42 s", /// the mapping should be of a type or . class AsNumberWithUnit : public Validator { public: /// Adjust AsNumberWithUnit behavior. /// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched. /// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError /// if UNIT_REQUIRED is set and unit literal is not found. enum Options { CASE_SENSITIVE = 0, CASE_INSENSITIVE = 1, UNIT_OPTIONAL = 0, UNIT_REQUIRED = 2, DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL }; template explicit AsNumberWithUnit(std::map mapping, Options opts = DEFAULT, const std::string &unit_name = "UNIT") { description(generate_description(unit_name, opts)); validate_mapping(mapping, opts); // transform function func_ = [mapping, opts](std::string &input) -> std::string { Number num{}; detail::rtrim(input); if(input.empty()) { throw ValidationError("Input is empty"); } // Find split position between number and prefix auto unit_begin = input.end(); while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) { --unit_begin; } std::string unit{unit_begin, input.end()}; input.resize(static_cast(std::distance(input.begin(), unit_begin))); detail::trim(input); if(opts & UNIT_REQUIRED && unit.empty()) { throw ValidationError("Missing mandatory unit"); } if(opts & CASE_INSENSITIVE) { unit = detail::to_lower(unit); } if(unit.empty()) { using CLI::detail::lexical_cast; if(!lexical_cast(input, num)) { throw ValidationError(std::string("Value ") + input + " could not be converted to " + detail::type_name()); } // No need to modify input if no unit passed return {}; } // find corresponding factor auto it = mapping.find(unit); if(it == mapping.end()) { throw ValidationError(unit + " unit not recognized. " "Allowed values: " + detail::generate_map(mapping, true)); } if(!input.empty()) { using CLI::detail::lexical_cast; bool converted = lexical_cast(input, num); if(!converted) { throw ValidationError(std::string("Value ") + input + " could not be converted to " + detail::type_name()); } // perform safe multiplication bool ok = detail::checked_multiply(num, it->second); if(!ok) { throw ValidationError(detail::to_string(num) + " multiplied by " + unit + " factor would cause number overflow. Use smaller value."); } } else { num = static_cast(it->second); } input = detail::to_string(num); return {}; }; } private: /// Check that mapping contains valid units. /// Update mapping for CASE_INSENSITIVE mode. template static void validate_mapping(std::map &mapping, Options opts) { for(auto &kv : mapping) { if(kv.first.empty()) { throw ValidationError("Unit must not be empty."); } if(!detail::isalpha(kv.first)) { throw ValidationError("Unit must contain only letters."); } } // make all units lowercase if CASE_INSENSITIVE if(opts & CASE_INSENSITIVE) { std::map lower_mapping; for(auto &kv : mapping) { auto s = detail::to_lower(kv.first); if(lower_mapping.count(s)) { throw ValidationError(std::string("Several matching lowercase unit representations are found: ") + s); } lower_mapping[detail::to_lower(kv.first)] = kv.second; } mapping = std::move(lower_mapping); } } /// Generate description like this: NUMBER [UNIT] template static std::string generate_description(const std::string &name, Options opts) { std::stringstream out; out << detail::type_name() << ' '; if(opts & UNIT_REQUIRED) { out << name; } else { out << '[' << name << ']'; } return out.str(); } }; inline AsNumberWithUnit::Options operator|(const AsNumberWithUnit::Options &a, const AsNumberWithUnit::Options &b) { return static_cast(static_cast(a) | static_cast(b)); } /// Converts a human-readable size string (with unit literal) to uin64_t size. /// Example: /// "100" => 100 /// "1 b" => 100 /// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024) /// "10 KB" => 10240 /// "10 kb" => 10240 /// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024) /// "10kb" => 10240 /// "2 MB" => 2097152 /// "2 EiB" => 2^61 // Units up to exibyte are supported class AsSizeValue : public AsNumberWithUnit { public: using result_t = std::uint64_t; /// If kb_is_1000 is true, /// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024 /// (same applies to higher order units as well). /// Otherwise, interpret all literals as factors of 1024. /// The first option is formally correct, but /// the second interpretation is more wide-spread /// (see https://en.wikipedia.org/wiki/Binary_prefix). explicit AsSizeValue(bool kb_is_1000); private: /// Get mapping static std::map init_mapping(bool kb_is_1000); /// Cache calculated mapping static std::map get_mapping(bool kb_is_1000); }; namespace detail { /// Split a string into a program name and command line arguments /// the string is assumed to contain a file name followed by other arguments /// the return value contains is a pair with the first argument containing the program name and the second /// everything else. CLI11_INLINE std::pair split_program_name(std::string commandline); } // namespace detail /// @} CLI11_INLINE std::string Validator::operator()(std::string &str) const { std::string retstring; if(active_) { if(non_modifying_) { std::string value = str; retstring = func_(value); } else { retstring = func_(str); } } return retstring; } CLI11_NODISCARD CLI11_INLINE Validator Validator::description(std::string validator_desc) const { Validator newval(*this); newval.desc_function_ = [validator_desc]() { return validator_desc; }; return newval; } CLI11_INLINE Validator Validator::operator&(const Validator &other) const { Validator newval; newval._merge_description(*this, other, " AND "); // Give references (will make a copy in lambda function) const std::function &f1 = func_; const std::function &f2 = other.func_; newval.func_ = [f1, f2](std::string &input) { std::string s1 = f1(input); std::string s2 = f2(input); if(!s1.empty() && !s2.empty()) return std::string("(") + s1 + ") AND (" + s2 + ")"; return s1 + s2; }; newval.active_ = active_ && other.active_; newval.application_index_ = application_index_; return newval; } CLI11_INLINE Validator Validator::operator|(const Validator &other) const { Validator newval; newval._merge_description(*this, other, " OR "); // Give references (will make a copy in lambda function) const std::function &f1 = func_; const std::function &f2 = other.func_; newval.func_ = [f1, f2](std::string &input) { std::string s1 = f1(input); std::string s2 = f2(input); if(s1.empty() || s2.empty()) return std::string(); return std::string("(") + s1 + ") OR (" + s2 + ")"; }; newval.active_ = active_ && other.active_; newval.application_index_ = application_index_; return newval; } CLI11_INLINE Validator Validator::operator!() const { Validator newval; const std::function &dfunc1 = desc_function_; newval.desc_function_ = [dfunc1]() { auto str = dfunc1(); return (!str.empty()) ? std::string("NOT ") + str : std::string{}; }; // Give references (will make a copy in lambda function) const std::function &f1 = func_; newval.func_ = [f1, dfunc1](std::string &test) -> std::string { std::string s1 = f1(test); if(s1.empty()) { return std::string("check ") + dfunc1() + " succeeded improperly"; } return std::string{}; }; newval.active_ = active_; newval.application_index_ = application_index_; return newval; } CLI11_INLINE void Validator::_merge_description(const Validator &val1, const Validator &val2, const std::string &merger) { const std::function &dfunc1 = val1.desc_function_; const std::function &dfunc2 = val2.desc_function_; desc_function_ = [=]() { std::string f1 = dfunc1(); std::string f2 = dfunc2(); if((f1.empty()) || (f2.empty())) { return f1 + f2; } return std::string(1, '(') + f1 + ')' + merger + '(' + f2 + ')'; }; } namespace detail { #if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 CLI11_INLINE path_type check_path(const char *file) noexcept { std::error_code ec; auto stat = std::filesystem::status(file, ec); if(ec) { return path_type::nonexistent; } switch(stat.type()) { case std::filesystem::file_type::none: // LCOV_EXCL_LINE case std::filesystem::file_type::not_found: return path_type::nonexistent; case std::filesystem::file_type::directory: return path_type::directory; case std::filesystem::file_type::symlink: case std::filesystem::file_type::block: case std::filesystem::file_type::character: case std::filesystem::file_type::fifo: case std::filesystem::file_type::socket: case std::filesystem::file_type::regular: case std::filesystem::file_type::unknown: default: return path_type::file; } } #else CLI11_INLINE path_type check_path(const char *file) noexcept { #if defined(_MSC_VER) struct __stat64 buffer; if(_stat64(file, &buffer) == 0) { return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file; } #else struct stat buffer; if(stat(file, &buffer) == 0) { return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file; } #endif return path_type::nonexistent; } #endif CLI11_INLINE ExistingFileValidator::ExistingFileValidator() : Validator("FILE") { func_ = [](std::string &filename) { auto path_result = check_path(filename.c_str()); if(path_result == path_type::nonexistent) { return "File does not exist: " + filename; } if(path_result == path_type::directory) { return "File is actually a directory: " + filename; } return std::string(); }; } CLI11_INLINE ExistingDirectoryValidator::ExistingDirectoryValidator() : Validator("DIR") { func_ = [](std::string &filename) { auto path_result = check_path(filename.c_str()); if(path_result == path_type::nonexistent) { return "Directory does not exist: " + filename; } if(path_result == path_type::file) { return "Directory is actually a file: " + filename; } return std::string(); }; } CLI11_INLINE ExistingPathValidator::ExistingPathValidator() : Validator("PATH(existing)") { func_ = [](std::string &filename) { auto path_result = check_path(filename.c_str()); if(path_result == path_type::nonexistent) { return "Path does not exist: " + filename; } return std::string(); }; } CLI11_INLINE NonexistentPathValidator::NonexistentPathValidator() : Validator("PATH(non-existing)") { func_ = [](std::string &filename) { auto path_result = check_path(filename.c_str()); if(path_result != path_type::nonexistent) { return "Path already exists: " + filename; } return std::string(); }; } CLI11_INLINE IPV4Validator::IPV4Validator() : Validator("IPV4") { func_ = [](std::string &ip_addr) { auto result = CLI::detail::split(ip_addr, '.'); if(result.size() != 4) { return std::string("Invalid IPV4 address must have four parts (") + ip_addr + ')'; } int num = 0; for(const auto &var : result) { using CLI::detail::lexical_cast; bool retval = lexical_cast(var, num); if(!retval) { return std::string("Failed parsing number (") + var + ')'; } if(num < 0 || num > 255) { return std::string("Each IP number must be between 0 and 255 ") + var; } } return std::string(); }; } } // namespace detail CLI11_INLINE FileOnDefaultPath::FileOnDefaultPath(std::string default_path, bool enableErrorReturn) : Validator("FILE") { func_ = [default_path, enableErrorReturn](std::string &filename) { auto path_result = detail::check_path(filename.c_str()); if(path_result == detail::path_type::nonexistent) { std::string test_file_path = default_path; if(default_path.back() != '/' && default_path.back() != '\\') { // Add folder separator test_file_path += '/'; } test_file_path.append(filename); path_result = detail::check_path(test_file_path.c_str()); if(path_result == detail::path_type::file) { filename = test_file_path; } else { if(enableErrorReturn) { return "File does not exist: " + filename; } } } return std::string{}; }; } CLI11_INLINE AsSizeValue::AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) { if(kb_is_1000) { description("SIZE [b, kb(=1000b), kib(=1024b), ...]"); } else { description("SIZE [b, kb(=1024b), ...]"); } } CLI11_INLINE std::map AsSizeValue::init_mapping(bool kb_is_1000) { std::map m; result_t k_factor = kb_is_1000 ? 1000 : 1024; result_t ki_factor = 1024; result_t k = 1; result_t ki = 1; m["b"] = 1; for(std::string p : {"k", "m", "g", "t", "p", "e"}) { k *= k_factor; ki *= ki_factor; m[p] = k; m[p + "b"] = k; m[p + "i"] = ki; m[p + "ib"] = ki; } return m; } CLI11_INLINE std::map AsSizeValue::get_mapping(bool kb_is_1000) { if(kb_is_1000) { static auto m = init_mapping(true); return m; } static auto m = init_mapping(false); return m; } namespace detail { CLI11_INLINE std::pair split_program_name(std::string commandline) { // try to determine the programName std::pair vals; trim(commandline); auto esp = commandline.find_first_of(' ', 1); while(detail::check_path(commandline.substr(0, esp).c_str()) != path_type::file) { esp = commandline.find_first_of(' ', esp + 1); if(esp == std::string::npos) { // if we have reached the end and haven't found a valid file just assume the first argument is the // program name if(commandline[0] == '"' || commandline[0] == '\'' || commandline[0] == '`') { bool embeddedQuote = false; auto keyChar = commandline[0]; auto end = commandline.find_first_of(keyChar, 1); while((end != std::string::npos) && (commandline[end - 1] == '\\')) { // deal with escaped quotes end = commandline.find_first_of(keyChar, end + 1); embeddedQuote = true; } if(end != std::string::npos) { vals.first = commandline.substr(1, end - 1); esp = end + 1; if(embeddedQuote) { vals.first = find_and_replace(vals.first, std::string("\\") + keyChar, std::string(1, keyChar)); } } else { esp = commandline.find_first_of(' ', 1); } } else { esp = commandline.find_first_of(' ', 1); } break; } } if(vals.first.empty()) { vals.first = commandline.substr(0, esp); rtrim(vals.first); } // strip the program name vals.second = (esp < commandline.length() - 1) ? commandline.substr(esp + 1) : std::string{}; ltrim(vals.second); return vals; } } // namespace detail /// @} class Option; class App; /// This enum signifies the type of help requested /// /// This is passed in by App; all user classes must accept this as /// the second argument. enum class AppFormatMode { Normal, ///< The normal, detailed help All, ///< A fully expanded help Sub, ///< Used when printed as part of expanded subcommand }; /// This is the minimum requirements to run a formatter. /// /// A user can subclass this is if they do not care at all /// about the structure in CLI::Formatter. class FormatterBase { protected: /// @name Options ///@{ /// The width of the first column std::size_t column_width_{30}; /// @brief The required help printout labels (user changeable) /// Values are Needs, Excludes, etc. std::map labels_{}; ///@} /// @name Basic ///@{ public: FormatterBase() = default; FormatterBase(const FormatterBase &) = default; FormatterBase(FormatterBase &&) = default; FormatterBase &operator=(const FormatterBase &) = default; FormatterBase &operator=(FormatterBase &&) = default; /// Adding a destructor in this form to work around bug in GCC 4.7 virtual ~FormatterBase() noexcept {} // NOLINT(modernize-use-equals-default) /// This is the key method that puts together help virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0; ///@} /// @name Setters ///@{ /// Set the "REQUIRED" label void label(std::string key, std::string val) { labels_[key] = val; } /// Set the column width void column_width(std::size_t val) { column_width_ = val; } ///@} /// @name Getters ///@{ /// Get the current value of a name (REQUIRED, etc.) CLI11_NODISCARD std::string get_label(std::string key) const { if(labels_.find(key) == labels_.end()) return key; return labels_.at(key); } /// Get the current column width CLI11_NODISCARD std::size_t get_column_width() const { return column_width_; } ///@} }; /// This is a specialty override for lambda functions class FormatterLambda final : public FormatterBase { using funct_t = std::function; /// The lambda to hold and run funct_t lambda_; public: /// Create a FormatterLambda with a lambda function explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {} /// Adding a destructor (mostly to make GCC 4.7 happy) ~FormatterLambda() noexcept override {} // NOLINT(modernize-use-equals-default) /// This will simply call the lambda function std::string make_help(const App *app, std::string name, AppFormatMode mode) const override { return lambda_(app, name, mode); } }; /// This is the default Formatter for CLI11. It pretty prints help output, and is broken into quite a few /// overridable methods, to be highly customizable with minimal effort. class Formatter : public FormatterBase { public: Formatter() = default; Formatter(const Formatter &) = default; Formatter(Formatter &&) = default; Formatter &operator=(const Formatter &) = default; Formatter &operator=(Formatter &&) = default; /// @name Overridables ///@{ /// This prints out a group of options with title /// CLI11_NODISCARD virtual std::string make_group(std::string group, bool is_positional, std::vector opts) const; /// This prints out just the positionals "group" virtual std::string make_positionals(const App *app) const; /// This prints out all the groups of options std::string make_groups(const App *app, AppFormatMode mode) const; /// This prints out all the subcommands virtual std::string make_subcommands(const App *app, AppFormatMode mode) const; /// This prints out a subcommand virtual std::string make_subcommand(const App *sub) const; /// This prints out a subcommand in help-all virtual std::string make_expanded(const App *sub) const; /// This prints out all the groups of options virtual std::string make_footer(const App *app) const; /// This displays the description line virtual std::string make_description(const App *app) const; /// This displays the usage line virtual std::string make_usage(const App *app, std::string name) const; /// This puts everything together std::string make_help(const App * /*app*/, std::string, AppFormatMode) const override; ///@} /// @name Options ///@{ /// This prints out an option help line, either positional or optional form virtual std::string make_option(const Option *opt, bool is_positional) const { std::stringstream out; detail::format_help( out, make_option_name(opt, is_positional) + make_option_opts(opt), make_option_desc(opt), column_width_); return out.str(); } /// @brief This is the name part of an option, Default: left column virtual std::string make_option_name(const Option *, bool) const; /// @brief This is the options part of the name, Default: combined into left column virtual std::string make_option_opts(const Option *) const; /// @brief This is the description. Default: Right column, on new line if left column too large virtual std::string make_option_desc(const Option *) const; /// @brief This is used to print the name on the USAGE line virtual std::string make_option_usage(const Option *opt) const; ///@} }; using results_t = std::vector; /// callback function definition using callback_t = std::function; class Option; class App; using Option_p = std::unique_ptr