--- /dev/null
+#-*-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
--- /dev/null
+---
+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.
--- /dev/null
+---
+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).
--- /dev/null
+---
+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.
--- /dev/null
+---
+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).
--- /dev/null
+---
+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 the latest 1.6.x release, or a 1.8.x release (otherwise, please upgrade)
+- [ ] you are running mu4e without any third-party extensions (otherwise, make sure you can reproduce without those)
+- [ ] you have read all of the above
--- /dev/null
+# 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?
--- /dev/null
+name: 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
+
+ - if: contains(matrix.os, 'macos')
+ name: macos-deps
+ run: |
+ brew install meson ninja libgpg-error libtool pkg-config glib gmime xapian
+
+ - name: configure
+ run: ./autogen.sh -Dguile=disabled -Db_sanitize=address
+
+ - name: build
+ run: make
+
+ - name: test
+ run: make test-verbose-if-fail
--- /dev/null
+www/mu
+mug
+/mu/mu
+/mu/mu-help-strings.h
+mug2
+.desktop
+*html
+.deps
+.libs
+autom4te*
+Makefile
+Makefile.in
+INSTALL
+aclocal.m4
+config.*
+configure
+install-sh
+depcomp
+libtool
+ltmain.sh
+
+# Added automatically by `autoreconf`
+m4/libtool.m4
+m4/ltoptions.m4
+m4/ltsugar.m4
+m4/ltversion.m4
+m4/lt~obsolete.m4
+
+missing
+nohup.out
+vgdump
+stamp-h1
+GPATH
+GRTAGS
+GSYMS
+GTAGS
+*.lo
+*.o
+*.la
+*.x
+*.go
+*.gz
+*.bz2
+\#*
+*.aux
+*.cp
+*.fn
+*.info
+*.ky
+*.log
+*.pg
+*.toc
+*.tp
+*.vr
+*.elc
+*.gcda
+*.gcno
+*.trs
+*.exe
+*.lib
+aminclude_static.am
+elisp-comp
+elc-stamp
+dummy.cc
+msg2pdf
+gmime-test
+test-mu-cmd
+test-mu-cmd-cfind
+test-mu-contacts
+test-mu-container
+test-mu-date
+test-mu-flags
+test-mu-maildir
+test-mu-msg
+test-mu-msg-fields
+test-mu-query
+test-mu-runtime
+test-mu-store
+test-mu-str
+test-mu-threads
+test-mu-util
+test-parser
+test-tokenizer
+test-utils
+tokenize
+test-command-parser
+test-mu-utils
+test-sexp-parser
+test-scanner
+/guile/tests/test-mu-guile
+
+mu4e-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
--- /dev/null
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
--- /dev/null
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null
+See NEWS.org
--- /dev/null
+## 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 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+if BUILD_GUILE
+guile=guile
+else
+guile=
+endif
+
+if BUILD_MU4E
+mu4e=mu4e
+else
+mu4e=
+endif
+
+SUBDIRS=m4 man lib $(guile) mu $(mu4e) contrib
+
+ACLOCAL_AMFLAGS=-I m4
+
+# so we can say 'make test'
+check: test cleanupnote
+
+cleanupnote:
+ @echo -e "\nNote: you can remove the mu-test-<uid> dir in your tempdir"
+ @echo "after 'make check' has finished."
+
+tags:
+ gtags
+
+EXTRA_DIST= \
+ TODO \
+ README.org \
+ gtest.mk \
+ NEWS \
+ NEWS.org \
+ autogen.sh
+
+doc_DATA = \
+ NEWS.org
+
+include $(top_srcdir)/aminclude_static.am
+
+CODE_COVERAGE_IGNORE_PATTERN= \
+ '/usr/*' \
+ '*test-*'
--- /dev/null
+## 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 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
+
+
+.PHONY: all
+.PHONY: check test test-verbose-if-fail test-valgrind test-helgrind
+.PHONY: benchmark coverage
+.PHONY: dist install 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)
+
+$(BUILDDIR):
+ $(MESON) $(MESON_FLAGS) $(BUILDDIR)
+
+check: test
+
+test: all
+ $(MESON) test $(VERBOSE) -C $(BUILDDIR)
+
+
+install: $(BUILDDIR)
+ @cd $(BUILDDIR); $(MESON) install
+
+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
+
+test-valgrind: $(BUILDDIR)
+ @cd $(BUILDDIR); $(MESON) test \
+ --wrap='valgrind --leak-check=full --error-exitcode=1' \
+ --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 --html --css-ref=texinfo-klare.css -o ${HTMLPATH} mu4e/mu4e.texi
--- /dev/null
+See NEWS.org
+
--- /dev/null
+#+STARTUP:showall
+* NEWS (user visible changes & bigger non-visible ones)
+
+* 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).
+
+* Old news
+ :PROPERTIES:
+ :VISIBILITY: folded
+ :END:
+
+** 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.5
+
+*** mu
+
+ - allow 'contact:' as a shortcut in queries for 'from:foo OR to:foo OR
+ cc:foo OR bcc:foo', and 'recip:' as a shortcut for 'to:foo OR cc:foo OR
+ bcc:foo'
+ - support getting related messages (--include-related), which includes
+ messages that may not match the query, but that are in the same threads as
+ messages that were
+ - support "list:"/"v:" for matching mailing list names, and the "v"
+ format-field to show them. E.g 'mu find list:emacs-orgmode.gnu.org'
+
+*** mu4e
+
+ - scroll down in message view takes you to next message (but see
+ `mu4e-view-scroll-to-next')
+ - support 'human dates', that is, show the time for today's messages, and
+ the date for older messages in the headers view
+ - replace `mu4e-user-mail-address-regexp' and `mu4e-my-mail-addresses' with
+ `mu4e-user-mail-address-list'
+ - support tags (i.e.., X-Keywords and friends) in the headers-view, and the
+ message view. Thanks to Abdó Roig-Maranges. New field ":tags".
+ - automatically update the headers buffer when new messages are found during
+ indexing; set `mu4e-headers-auto-update' to nil to disable this.
+ - update mail/index with M-x mu4e-update-mail-and-index; which everywhere in
+ mu4e is available with key C-S-u. Use prefix argument to run in
+ background.
+ - add function `mu4e-update-index' to only update the index
+ - add 'friendly-names' for mailing lists, so they should up nicely in the
+ headers view
+
+*** guile
+
+ - add 'mu script' command to run mu script, for example to do statistics on
+ your message corpus. See the mu-script man-page.
+
+*** mug
+
+ - ported to gtk+ 3; remove gtk+ 2.x code
+
+
+
+** Release 0.9.9 <2012-10-14>
+
+*** mu4e
+ - view: address can be toggled long/short, compose message
+ - sanitize opening urls (mouse-1, and not too eager)
+ - tooltips for header labels, flags
+ - add sort buttons to header-labels
+ - support signing / decryption of messages
+ - improve address-autocompletion (e.g., ensure it's case-insensitive)
+ - much faster when there are many maildirs
+ - improved line wrapping
+ - better handle attached messages
+ - improved URL-matching
+ - improved messages to user (mu4e-(warn|error|message))
+ - add refiling functionality
+ - support fancy non-ascii in the UI
+ - dynamic folders (i.e.., allow mu4e-(sent|draft|trash|refile)-folder) to
+ be a function
+ - dynamic attachment download folder (can be a function now)
+ - much improved manual
+
+*** mu
+ - remove --summary (use --summary-len instead)
+ - add --after for mu find, to limit to messages after T
+ - add new command `mu verify', to verify signatures
+ - fix iso-2022-jp decoding (and other 7-bit clean non-ascii)
+ - add support for X-keywords
+ - performance improvements for threaded display (~ 25% for 23K msgs)
+ - mu improved user-help (and the 'mu help' command)
+ - toys/mug2 replaces toys/mug
+
+*** mu-guile
+ - automated tests
+ - add mu:timestamp, mu:count
+ - handle db reopenings in the background
+
+
+** Release 0.9.8.5 <2012-07-01>
+
+*** mu4e
+
+ - auto-completion of e-mail addresses
+ - inline display of images (see `mu4e-view-show-images'), uses imagemagick
+ if available
+ - interactively change number of headers / columns for showing headers with
+ C-+ and C-- in headers, view mode
+ - support flagging message
+ - navigate to previous/next queries like a web browser (with <M-left>,
+ <M-right>)
+ - 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=<n> 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:
--- /dev/null
+#+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://melpa.org/#/?q=mu4e&sort=version&asc=false][https://img.shields.io/badge/Emacs-25.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]]
+
+Welcome to ~mu~!
+
+*Note*: you are looking at the *development* branch, which is where new code is
+being developed and tested, and which may occasionally break.
+
+Distribution and non-adventurous users are instead recommended to use the [[https://github.com/djcb/mu/tree/release/1.8][1.8
+Release Branch]] or to pick up one of the [[https://github.com/djcb/mu/releases][1.8 Releases]].
+
+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 and C++; ~mu4e~ is written in ~elisp~ and ~mu-guile~ in a mix of C++ and
+Scheme.
+
+Note, ~mu~ is available in Linux distributions (e.g. Debian/Ubuntu and Fedora)
+under the name ~maildir-utils~; apparently because they don't like short names.
+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~.
+
+** Building
+
+#+begin_example
+$ git clone git://github.com/djcb/mu.git
+$ cd mu
+#+end_example
+
+Now, you have a choice. ~mu~ uses ~meson~ for building, but includes a good-old
+~Makefile~ with some useful targets, which should work for typical cases.
+
+#+begin_example
+$ ./autogen.sh && make
+$ sudo make install
+#+end_example
+
+Alternatively, for more control, 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.
+
+
+
--- /dev/null
+#+STARTUP: showall
+
+* TODO (fixes, ideas, etc.)
+
+** Future stuff
+
+*** mu
+
+ - put threading information in the database, and enable getting the complete
+ threads when searching
+ - refactor fill_database function in test cases
+ - don't show duplicate e-mails (i.e.. for Gmail); check the message-id
+
+*** mu-guile
+
+ - move contact export to separate scm
+ - fix logging
+
+*** mu4e
+
+ - special-case replying to messages sent by self
+ - identities (see Jacek's 'mu4e: From field in replies' mail)
+ ==> [ workaround available, using mu4e-pre-compose-hook, dynamic folders ]
+ - new-mail warning
+ ==> [ workaround available, using mu4e-index-updated-hook ]
+ - custom header fields in headers-view, message-view
+ - show maildirs as a tree, not a list in speed bar
+ - review emacs menus
+ - re-factor / separate window/buffer management
+ - enable keeping message view buffers around
+ - better naming for draft/view buffers
+ - header updating interferes with marks (when updating for 'mark as read',
+ when reading a marked message)
+ - set/unset flag editing command
+ - handling of database upgrades
+ - restore point after rerunning a search
+ - make the mu4e-bookmarks format similar to the other ones
+ - refresh current query after update?
+ - fix mu4e-mark-set to work from the view buffer as well
+ - open links to mails through headers-mode somehow (i.e..,
+ mu4e-view-message-with-msgid)
+ - improve mouse interaction (i.e., cursor vs point)
+ - show counts of messages in searches (in main view)
+ - show flush only if there's something to flush (and # of flushables)
+ - fix unsafe temp-file handling
+ - make copy paste name/address in mu4e-view possible
+
+
+* Done (0.9.9.x)
+
+ - mu4e: scroll down –> go to next message
+ - mu: add contact: as a shortcut for matching from/to/cc/bcc:
+ - guile integration
+ - statistics
+ - 'human' dates in the headers view
+ - :tags in headers, message view
+
+* Done
+ :PROPERTIES:
+ :VISIBILITY: folded
+ :END:
+
+
+** Done (0.9.9)
+
+ - make contacts in the view clickable (toggle long/short display, compose message)
+ - opening urls is too eager (now use M-RET for opening url at point, not just
+ RET, which conflicted with using RET for scrolling)
+ - document quoting of queries
+ - use mu-error
+ - tooltips in header labels
+ - tooltip for flags field
+ - remove --summary option (for mu find, mu view); use --summary-len instead
+ - add sort buttons to header labels (and do the sorting)
+ - cleanup mu-cmd-find
+ - implement --after for mu find, to only show message files changed after a
+ certain time (mtime)
+ - add mu:timestamp for guile (referring to the message file's mtime)
+ - guile automated tests
+ - add 'mu verify'
+ - automated tests
+ - handle verbose/quiet/normal output 'mu verify'
+ - check gmime 2.4 does not break
+ - hook up mu4e with 'mu verify'
+ - add 'help' command
+ - refactor mu-msg-part
+ - move widgets/ into toys/mug2, remove toys/mug/, rename toys/mug2 -> toys/mug
+ - add guile mu:count
+ - don't show GPG/PKCS7 sigs as attachments
+ - fix address completion (quote names)
+ - add support for X-Keywords (in addition to X-Label)
+ - guile: add stats test cases
+ - fixed iso-2022-jp (japanese) decoding
+ - make address completion case-insensitive
+ - recognize '*' in urls
+ - handle exception 'The revision being read has been discarded - you should
+ call Xapian::Database::reopen() and retry the operation'
+ - handle passwords from get-mail shell command
+ - support fancy (non-ascii) chars for header flags, thread prefix strings
+ - improve performance of getting the list of maildirs
+ - fix setting wrapped/hide state in viewer
+ - fix ' realpath() failed for...' stuff
+ - allow for fancy chars (> ascii), make it configurable (mu4e-use-fancy-chars)
+ - don't user `error' for user-errors
+ - better echo-area reporting
+ - improve help feedback for user (command line)
+ - handling of encrypted messages
+ - improved checked for gmime-2.6 crypto funcs
+ - handling of command line options / help
+ - fix / add support for :size
+ - mu4e~view-wrap-lines (use visual-line-mode? see Jacek's mu4e~view-wrap-lines
+ mail)
+ - better help
+ - threading optimizations
+ - actions for /all/ headers, actions for /all/ attachment
+ - handle attached messages with attachments
+
+** Done (0.8.9.5)
+
+ - make next/prev header respect prefix argument (Jacek's patch)
+ - make search results a stack (well, multiple stacks)
+ - optionally keep cc with user's email
+ - enable setting/unsetting 'Flagged' on messages
+ - allow narrowing of search results
+ - interactive split-view control (Jacek)
+ - view images inline
+ - *FIX* slow maildirs when there are many
+ - *FIX* ignore unrecognized maildir flag letters
+ - *FIX*: reply-to does not make it to the frontend
+ - *FIX* wrong buffer deleted after sending (see '(non mu) buffer is killed')
+ - rich text composing (with org-mode)
+ - let message-mode deal with burying/killing compose buffers
+ - *FIX* add runtime check for imagemagick
+ - *FIX* no error note if target message already exists (when moving)
+ - sorting + show / hide threads
+ - *FIX* having multiple header views visible
+ - *FIX* fix for strings where len (g_utf8_strdown (str)) > len (str)
+ - make sure marks correspond to the *current* message in message view (see
+ https://github.com/djcb/mu/issues/26)
+ - *FIX* don't remove unknown message flags when moving
+ - make guile/gtk/webkit dependency optional
+ - improve fringe marks (see https://github.com/djcb/mu/issues/21)
+ - mark message, decide what to do with them later (i.e.. 'deferred marking')
+ - custom predicate functions for marking
+ - make mu4e buffer killing less aggressive (i.e.., DWIM)
+ - about mu4e
+ - hide some headers when composing
+ - fix sorting subjects with ':' (but not 'Re:' or 'Fwd:')
+ - strip signature from original when replying
+ - make refresh after changing sort, threads the default
+ - contact completion (see Jacek's 'mu4e: using' mail)
+ - *FIX* emacs23 mailto: handling
+ - *FIX* message interference
+ - *FIX* emacs23.2+ auto-completion
+
+
+# Local Variables:
+# mode: org
+# End:
--- /dev/null
+#!/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
+
+# Add a Makefile with some useful target
+cp Makefile.meson Makefile
+
+echo "*** Now run 'ninja -C ${BUILDDIR}' to build mu"
+echo "*** Or check the Makefile for some useful targets"
--- /dev/null
+#!/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 ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir} \
+ ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir}/${infofile}
+fi
+
+gzip --force ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir}/${infofile}
--- /dev/null
+## 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 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+AC_PREREQ([2.68])
+AC_INIT([mu],[1.8.13],[https://github.com/djcb/mu/issues],[mu])
+AC_COPYRIGHT([Copyright (C) 2008-2022 Dirk-Jan C. Binnema])
+AC_CONFIG_HEADERS([config.h])
+AC_CONFIG_SRCDIR([mu/mu.cc])
+# libtoolize wants to put some stuff in here; if you have an old
+# autotools/libtool setup. you can try to comment this out
+AC_CONFIG_MACRO_DIR([m4])
+AC_CONFIG_AUX_DIR([build-aux])
+
+AM_INIT_AUTOMAKE([1.14 foreign no-dist-gzip tar-ustar dist-xz])
+
+# silent build if we have a new enough automake
+m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
+
+AS_IF([test x$prefix = xNONE],[prefix=/usr/local])
+AC_SUBST(prefix)
+
+# AC_PROG_CXX *before* AC_PROG_CC, otherwise configure won't error out
+# when a c++ compiler is not found. Weird, huh?
+AC_PROG_CXX
+AC_PROG_CC
+AC_PROG_CPP
+AC_PROG_INSTALL
+AC_CHECK_INCLUDES_DEFAULT
+AC_PROG_EGREP
+
+
+extra_flags="-Wformat-security \
+ -Wstack-protector \
+ -Wstack-protector-all \
+ -Wno-cast-function-type \
+ -Wno-bad-function-cast \
+ -Wno-switch-enum"
+
+AX_CXX_COMPILE_STDCXX_17
+AX_COMPILER_FLAGS_CXXFLAGS([],[],[${extra_cflags}])
+AX_APPEND_COMPILE_FLAGS([-Wno-inline],[CXXFLAGS])
+AX_VALGRIND_CHECK
+
+LT_INIT
+
+AX_CODE_COVERAGE
+
+AC_PROG_AWK
+AC_CHECK_PROG(SORT,sort,sort)
+
+AC_CHECK_HEADERS([wordexp.h])
+
+# use the 64-bit versions
+AC_SYS_LARGEFILE
+
+# asan is somewhat similar to valgrind, but has low enough overhead so it
+# can be used during normal operation.
+AC_ARG_ENABLE([asan],[AS_HELP_STRING([--enable-asan],
+ [Enable Address Sanitizer])], [use_asan=$enableval], [use_asan=no])
+AS_IF([test "x$use_asan" = "xyes"],[
+ AC_SUBST(ASAN_CFLAGS, "-fsanitize=address -static-libasan -fno-omit-frame-pointer")
+ AC_SUBST(ASAN_CXXFLAGS,"-fsanitize=address -static-libasan -fno-omit-frame-pointer")
+ AC_SUBST(ASAN_LDFLAGS, "-fsanitize=address -static-libasan -fno-omit-frame-pointer")
+])
+
+
+# check for makeinfo
+AC_CHECK_PROG(have_makeinfo,makeinfo,yes,no)
+AM_CONDITIONAL(HAVE_MAKEINFO,test "x$have_makeinfo" = "xyes")
+AM_COND_IF(HAVE_MAKEINFO,[],[
+ # seems build *insists* on trying to makeinfo, erroring out
+ # if it does not exist. Let's work around that.
+ AC_SUBST(MAKEINFO,[true])
+])
+
+# we need emacs for byte-compiling mu4e
+build_mu4e=no
+AC_ARG_ENABLE([mu4e],
+ AS_HELP_STRING([--disable-mu4e],[Disable building mu4e]))
+AS_IF([test "x$enable_mu4e" != "xno"], [
+ AM_PATH_LISPDIR
+ AS_IF([test "x$lispdir" != "xno"], [
+ emacs_version="$($EMACS --version | head -1)"
+ lispdir="${lispdir}/mu4e/"
+ ])
+ AS_CASE([$emacs_version],
+ [*25.3*],[build_mu4e=yes],
+ [*26*|*27*|*28*|*29*],[build_mu4e=yes],
+ [AC_MSG_WARN(emacs is too old to build mu4e (need emacs >= 25.3))])
+])
+AM_CONDITIONAL(BUILD_MU4E, test "x$build_mu4e" = "xyes")
+
+# we need some special tricks for filesystems that don't have d_type;
+# e.g. Solaris. See mu-maildir.c. Explicitly disabling it is for
+# testing purposes only
+AC_ARG_ENABLE([dirent-d-type],
+ AS_HELP_STRING([--disable-dirent-d-type],[Don't use dirent->d_type, even if you have it]),
+ [], [AC_STRUCT_DIRENT_D_TYPE]
+)
+AS_IF([test "x$ac_cv_member_struct_dirent_d_type" != "xyes"],
+ [use_dirent_d_type="no"], [use_dirent_d_type="yes"])
+
+# support for d_ino (inode) in struct dirent is optional; if it's
+# available we can sort direntries by inode and access them in that
+# order; this is much faster on some file systems (such as extfs3).
+# Explicitly disabling it is for testing purposes only.
+AC_ARG_ENABLE([dirent-d-ino],
+ AS_HELP_STRING([--disable-dirent-d-ino],[Don't use dirent->d_ino, even if you have it]),
+ [], [AC_STRUCT_DIRENT_D_INO]
+)
+AS_IF([test "x$ac_cv_member_struct_dirent_d_ino" != "xyes"],
+ [use_dirent_d_ino="no"], [use_dirent_d_ino="yes"])
+
+AC_CHECK_FUNCS([memset memcpy realpath setlocale strerror getpass setsid])
+AC_CHECK_FUNCS([vasprintf strptime])
+# timegm is no longer used in the source
+# AC_CHECK_FUNC(timegm,[],AC_MSG_ERROR([missing required function timegm]))
+
+# require pkg-config >= 0.28 (release in 2013; should be old enough...)
+# with that version, we don't need the AC_SUBST stuff after PKG_CHECK.
+m4_ifndef([PKG_PROG_PKG_CONFIG],
+ [m4_fatal([please install pkg-config >= 0.28 before running autoconf/autogen])])
+PKG_PROG_PKG_CONFIG(0.28) # latest version in buildroot
+AS_IF([test -z "$PKG_CONFIG"],
+ AC_MSG_ERROR([
+ *** pkg-config with version >= 0.28 could not be found.
+ ***
+ *** Make sure it is in your path, or set the PKG_CONFIG environment variable
+ *** to the full path to pkg-config.])
+)
+
+# glib2?
+PKG_CHECK_MODULES(GLIB,glib-2.0 >= 2.58 gobject-2.0 gio-2.0)
+glib_version="$($PKG_CONFIG --modversion glib-2.0)"
+
+# gmime, version 3.0 or higher
+PKG_CHECK_MODULES(GMIME,gmime-3.0)
+gmime_version="$($PKG_CONFIG --modversion gmime-3.0)"
+
+# xapian checking - we need 1.4 at least
+PKG_CHECK_MODULES(XAPIAN,xapian-core >= 1.4,[
+ have_xapian=yes
+ xapian_version=$($PKG_CONFIG xapian-core --modversion)
+ AC_SUBST(XAPIAN_CXXFLAGS,${XAPIAN_CFLAGS})
+],[
+ # fall back to the xapian-config script. Not sure if there are cases where the
+ # pkgconfig does not work, but xapian-config does, so keep this for now.
+ AC_MSG_NOTICE([falling back to xapian-config])
+ AC_CHECK_PROG(XAPIAN_CONFIG,xapian-config,xapian-config,no)
+ AS_IF([test "x$XAPIAN_CONFIG" = "xno"],[
+ AC_MSG_ERROR([
+ *** xapian could not be found; please install it
+ *** e.g., in debian/ubuntu the package would be 'libxapian-dev'
+ *** If you compiled it yourself, you should ensure that xapian-config
+ *** is in your PATH.])],
+ [xapian_version=$($XAPIAN_CONFIG --version | sed -e 's/.* //')])
+
+ AS_CASE([$xapian_version],
+ [1.[[4-9]].[[0-9]]*],
+ [AC_MSG_NOTICE([xapian $xapian_version found.])],
+ [AC_MSG_ERROR([*** xapian version >= 1.4 needed, but version $xapian_version found.])])
+
+ XAPIAN_CXXFLAGS="$($XAPIAN_CONFIG --cxxflags)"
+ XAPIAN_LIBS="$($XAPIAN_CONFIG --libs)"
+ have_xapian="yes"
+
+ AC_SUBST(XAPIAN_CXXFLAGS)
+ AC_SUBST(XAPIAN_LIBS)
+])
+###############################################################################
+# we set the set the version of the MuStore (Xapian database) layout
+# here; it will become part of the db name, so we can automatically
+# recreate the database when we have incompatible changes.
+#
+# note that MU_STORE_SCHEMA_VERSION does not follow mu versioning, as we
+# hopefully don't have updates for each version; also, this has nothing to do
+# with Xapian's software versionx
+AC_DEFINE(MU_STORE_SCHEMA_VERSION,["465"],['Schema' version of the database])
+###############################################################################
+
+################################################################################
+# should we try to build an emacs dynamic module?
+#AC_CHECK_HEADER([emacs-module.h],[
+# AC_DEFINE([HAVE_EMACS_MODULE_H],[1], [Whether we have the emacs-module header])],
+# AC_MSG_NOTICE([emacs-module.h not found; not building module])
+#)
+#AM_CONDITIONAL([BUILD_EMACS_MODULE],[test "x$ac_cv_header_emacs_module_h" != "x"])
+################################################################################
+
+###############################################################################
+# build with guile 3.0/2.2 when available and not disabled.
+AC_ARG_ENABLE([guile], AS_HELP_STRING([--disable-guile],[Disable guile]))
+AS_IF([test "x$enable_guile" != "xno"],[
+
+ PKG_CHECK_MODULES(GUILE, [guile-3.0], [have_guile=yes],[
+ PKG_CHECK_MODULES(GUILE, [guile-2.2], [have_guile=yes], [have_guile=no])])
+ AS_IF([test "x$have_guile" = "xyes"],[
+ GUILE_PKG([3.0 2.2])
+ GUILE_PROGS
+ GUILE_FLAGS
+ AC_DEFINE_UNQUOTED([GUILE_BINARY],"$GUILE",[guile binary])
+ vsnarf=guile-snarf${GUILE_EFFECTIVE_VERSION}
+ AC_CHECK_PROGS(GUILE_SNARF,[${vsnarf} guile-snarf], [no])
+ guile_version=$($PKG_CONFIG guile-$GUILE_EFFECTIVE_VERSION --modversion)
+ ])
+])
+
+AM_CONDITIONAL(BUILD_GUILE,[test "x$have_guile" = "xyes" -a \
+ "x$ac_cv_prog_GUILE_SNARF" != "xno"])
+AM_COND_IF([BUILD_GUILE],[AC_DEFINE(BUILD_GUILE,[1], [Do we support Guile?])])
+###############################################################################
+
+###############################################################################
+# optional readline
+AC_ARG_ENABLE([readline], AS_HELP_STRING([--disable-readline],[Disable readline]))
+AS_IF([test "x$enable_readline" != "xno"], [
+ saved_libs=$LIBS
+ AX_LIB_READLINE
+ AC_SUBST(READLINE_LIBS,${LIBS})
+ LIBS=$saved_libs
+])
+###############################################################################
+
+###############################################################################
+# check for makeinfo
+AC_CHECK_PROG(have_makeinfo,makeinfo,yes,no)
+AM_CONDITIONAL(HAVE_MAKEINFO, [test "x$have_makeinfo" = "xyes"])
+###############################################################################
+
+###############################################################################
+# docdir, so we can use it in mu4e-meta.el.in
+AC_SUBST(MU_DOC_DIR, "${prefix}/share/doc/mu")
+###############################################################################
+
+AC_CONFIG_FILES([
+Makefile
+mu/Makefile
+lib/Makefile
+lib/doxyfile
+lib/thirdparty/Makefile
+lib/utils/Makefile
+lib/message/Makefile
+lib/index/Makefile
+mu4e/Makefile
+mu4e/mu4e-config.el
+guile/Makefile
+guile/mu/Makefile
+guile/examples/Makefile
+guile/scripts/Makefile
+man/Makefile
+m4/Makefile
+contrib/Makefile
+])
+AC_CONFIG_FILES([mu/mu-memcheck], [chmod +x mu/mu-memcheck])
+
+AC_OUTPUT
+
+dnl toys/msg2pdf/Makefile
+
+echo
+echo "mu configuration is complete."
+echo "------------------------------------------------"
+
+echo "mu version : $VERSION"
+echo
+echo "Xapian version : $xapian_version"
+echo "GLib version : $glib_version"
+echo "GMime version : $gmime_version"
+
+AM_COND_IF([BUILD_GUILE],[
+echo "Guile version : $guile_version"
+])
+echo
+echo "Have direntry->d_ino : $use_dirent_d_ino"
+echo "Have direntry->d_type : $use_dirent_d_type"
+echo "------------------------------------------------"
+echo
+
+#
+# Warnings / notes
+#
+
+# makeinfo
+if test "x$have_makeinfo" != "xyes"; then
+ echo "* You do not seem to have the makeinfo program; if you are building from git"
+ echo " you need that to create documentation for guile and emacs. It is in the"
+ echo " texinfo package in debian/ubuntu/fedora/... "
+ echo
+fi
+
+# wordexp
+AS_IF([test "x$ac_cv_header_wordexp_h" != "xyes"],[
+ echo "* Your system does not seem to have the 'wordexp' function."
+ echo " This means that you cannot use shell-like expansion in options and "
+ echo " some other places. So, for example, instead of"
+ echo " --maildir=~/Maildir"
+ echo " you should use the complete path, something like:"
+ echo " --maildir=/home/user/Maildir"
+])
+
+
+echo "NOTE: autotools support has been deprecated and will be removed"
+echo " after the next stable release. use the meson build instead"
+
+echo
+echo "Now, type 'make' (or 'gmake') to build mu"
+echo
--- /dev/null
+## Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+AM_CPPFLAGS=$(GMIME_CFLAGS) $(GLIB_CFLAGS) -I${prefix}/include
+AM_CFLAGS=-Wall -Wextra -Wno-unused-parameter -Wdeclaration-after-statement -pedantic
+
+
+EXTRA_DIST= \
+ mu-completion.zsh \
+ mu-sexp-convert \
+ mu.spec
--- /dev/null
+#compdef mu
+
+## Copyright (C) 2011-2012 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.
+
+# 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:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;; Copyright (C) 2012 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.
+
+;;
+;; a little hack to convert the output of
+;; mu find <expr> --format=sexp
+;; and
+;; mu view <expr> --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</~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 "<address>~a~a</address>"
+ (if (string? (car addr))
+ (format #f "<name>~a</name>"
+ (string->xml (car addr))) "")
+ (if (string? (cdr addr))
+ (format #f "<email>~a</email>"
+ (string->xml (cdr addr))) "")))
+ expr " "))
+ ((string= parent "parts") "<!-- message 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 "<item>~a</item>" (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 "<message>\n~a</message>\n" (convert-xml expr)) (msg->xml))
+ "")))))
+ (format #f "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<messages>\n~a</messages>" (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=<xml|json>\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:
--- /dev/null
+
+# 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 <ssaavedra@gpul.org> - 0.9.9.5-1
+- Create first SPEC.
--- /dev/null
+## Copyright (C) 2011 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.
+
+TEST_PROGS=
+
+
+# NOTE: we set the locale/tz to some well-know values, so the tests
+# (at least when running under 'make check') run in a predictable
+# environment. There are specific tests different timezone, though.
+#
+test: all $(TEST_PROGS)
+ @export LC_ALL="en_US.utf8"
+ @export TZ="Europe/Helsinki"
+ @test -z "$(TEST_PROGS)" || gtester --verbose $(TEST_PROGS) || exit $$?; \
+ test -z "$(SUBDIRS)" || \
+ for subdir in $(SUBDIRS); do \
+ test "$$subdir" = "." || \
+ (cd ./$$subdir && $(MAKE) $(AM_MAKEFLAGS) $@ ) || exit $$? ; \
+ done
+
+.PHONY: test gprof
--- /dev/null
+## 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 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+# note, we need top_builddir for snarfing with 'make distcheck' (ie.,
+# with separate builddir)
+SUBDIRS= . mu scripts examples
+
+AM_CPPFLAGS= \
+ -I. -I${top_builddir} -I${top_srcdir}/lib \
+ ${GUILE_CFLAGS} \
+ ${GLIB_CFLAGS}
+
+# don't use -Werror, as it might break on other compilers
+# use -Wno-unused-parameters, because some callbacks may not
+# really need all the params they get
+AM_CFLAGS= \
+ $(ASAN_CFLAGS) \
+ ${WARN_CFLAGS} \
+ -Wno-suggest-attribute=noreturn \
+ -Wno-missing-prototypes \
+ -Wno-missing-declarations
+
+AM_CXXFLAGS= \
+ $(ASAN_CXXFLAGS) \
+ ${WARN_CXXFLAGS} \
+ -Wno-redundant-decls \
+ -Wno-missing-declarations \
+ -Wno-suggest-attribute=noreturn
+
+lib_LTLIBRARIES= \
+ libguile-mu.la
+
+libguile_mu_la_SOURCES= \
+ mu-guile.cc \
+ mu-guile.hh \
+ mu-guile-message.cc \
+ mu-guile-message.hh
+
+libguile_mu_la_CFLAGS=$(AM_CFLAGS)
+libguile_mu_la_CXXFLAGS=$(AM_CXXFLAGS)
+
+libguile_mu_la_LIBADD= \
+ ${top_builddir}/lib/libmu.la \
+ ${top_builddir}/lib/utils/libmu-utils.la \
+ $(READLINE_LIBS) \
+ ${GUILE_LIBS}
+
+libguile_mu_la_LDFLAGS= \
+ $(ASAN_LDFLAGS) \
+ -shared \
+ -export-dynamic
+
+XFILES= \
+ mu-guile.x \
+ mu-guile-message.x
+
+info_TEXINFOS= \
+ mu-guile.texi
+mu_guile_TEXINFOS= \
+ fdl.texi
+
+# we include pre-snarfed files now; see meson.build for explanation
+#
+# BUILT_SOURCES=$(XFILES)
+# export CPP
+# snarfcxxopts= $(DEFS) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS)
+# SUFFIXES = .x .doc
+# .cc.x:
+# $(AM_V_GEN) $(GUILE_SNARF) -o $@ $< $(snarfcxxopts)
+#SNARF_DATA=$(XFILES)
+
+# FIXME: GUILE_SITEDIR would be better, but that
+# breaks 'make distcheck'
+scmdir=${prefix}/share/guile/site/${GUILE_EFFECTIVE_VERSION}
+scm_DATA=mu.scm
+
+EXTRA_DIST=$(scm_DATA) $(XFILES)
+
+## Add -MG to make the .x magic work with auto-dep code.
+MKDEP = $(CC) -M -MG $(snarfcppopts)
+
+#CLEANFILES=$(XFILES)
--- /dev/null
+#!/bin/sh
+## 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.
+
+@abs_builddir@/build-env @guild@ compile "$@"
+
+# Local-Variables:
+# mode: sh
+# End:
--- /dev/null
+## Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+EXTRA_DIST= \
+ msg-graphs \
+ contacts-export \
+ org2mu4e \
+ mu-biff
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2012 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.
+
+
+(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=<muhome>] "
+ "--format=<org-contact|mutt-alias|mutt-ab|wanderlust|quoted|plain(*)> "
+ "--sort-by=<frequency(*)|newness> [--revert] [--limit=<n>]\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:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2011-2012 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 (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=<muhome>] "
+ "--what=<per-hour|per-day|per-month|per-year-month|"
+ "per-year> [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:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2012 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.
+
+;; script to list the message matching <query> which are newer than
+;; <n> 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=<muhome>]"
+ " [--newer-than=<timestamp>] <query>"))
+ (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:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2011-2012 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.
+
+(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
+ "<no plain-text body>"
+ (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=<muhome>] [--tag=<tag>] <query>"))
+ (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:
--- /dev/null
+@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:
+
--- /dev/null
+## 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.
+
+#
+# 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)
+
+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()
+ meson.add_install_script(install_info_script, 'share/info', 'mu-guile.info')
+ endif
+endif
+
+guile_scm_dir=join_paths(datadir, 'guile', 'site', '3.0', 'mu')
+install_data(['mu.scm','mu/script.scm', 'mu/message.scm', 'mu/stats.scm', 'mu/plot.scm'],
+ install_dir: guile_scm_dir)
+
+
+mu_guile_scripts=[
+ join_paths('scripts', 'find-dups.scm'),
+ join_paths('scripts', 'msgs-count.scm'),
+ join_paths('scripts', 'msgs-per-day.scm'),
+ join_paths('scripts', 'msgs-per-hour.scm'),
+ join_paths('scripts', 'msgs-per-month.scm'),
+ join_paths('scripts', 'msgs-per-year-month.scm'),
+ join_paths('scripts', 'msgs-per-year.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()
+
+subdir('tests')
--- /dev/null
+/*
+** Copyright (C) 2011-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.
+**
+*/
+#include "mu-guile-message.hh"
+
+#include <libguile.h>
+#include "message/mu-message.hh"
+#include "utils/mu-utils.hh"
+#include <config.h>
+
+#include <glib-object.h>
+#include <memory>
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+
+#include "mu-guile.hh"
+
+#include <mu-runtime.hh>
+#include <mu-store.hh>
+#include <mu-query.hh>
+
+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<SCM, AllMessageFlagInfos.size()> 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<Message>;
+
+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<Message*>(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("#<msg ", port);
+
+ if (auto msg = message_from_scm(msg_smob); msg)
+ scm_puts(msg->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<size_t>(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);
+ }
+}
+#undef FUNC_NAME
+
+static SCM
+contacts_to_list(const Message& msg, Option<Field::Id> 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> 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;
+}
--- /dev/null
+/*
+** Copyright (C) 2011-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_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__*/
--- /dev/null
+/* 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);;
--- /dev/null
+/*
+** Copyright (C) 2011-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.
+**
+*/
+
+#include "mu-guile.hh"
+
+#include <config.h>
+#include <locale.h>
+#include <glib-object.h>
+
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+
+#include <mu-runtime.hh>
+#include <mu-store.hh>
+#include <mu-query.hh>
+
+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 : "<nameless>"),
+ 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<Mu::Store> StoreSingleton = Nothing;
+
+static bool
+mu_guile_init_instance(const char* muhome)
+try {
+ setlocale(LC_ALL, "");
+ if (!mu_runtime_init(muhome, "guile", true) || StoreSingleton)
+ return FALSE;
+
+ const auto path{mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB)};
+ auto store = Store::make(path);
+ if (!store) {
+ g_critical("error creating store @ %s: %s", path, 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_runtime_uninit();
+}
+
+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)) {
+ free(muhome);
+ mu_guile_error(FUNC_NAME, 0, "Failed to initialize mu", SCM_UNSPECIFIED);
+ }
+
+ g_debug("mu-guile: initialized @ %s", muhome ? muhome : "<default>");
+ 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, "<write_log>");
+ 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;
+}
--- /dev/null
+/*
+** Copyright (C) 2011-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_GUILE_H__
+#define __MU_GUILE_H__
+
+#include <glib.h>
+#include <libguile.h>
+#include <mu-query.hh>
+
+/**
+ * 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__*/
--- /dev/null
+\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 [<search-expression>])}
+@item @code{(mu:for-each-message <procedure> [<search-expression>])}
+@end itemize
+
+@noindent
+The first procedure, @code{mu:message-list} returns a list of all messages
+matching @t{<search-expression>}; if you leave @t{<search-expression>} 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 = (#<<mu:message> 9040640> #<<mu:message> 9040630>
+ #<<mu:message> 9040570>)
+@end verbatim
+
+@noindent
+Apparently, we have three messages matching @t{subject:coffee}, so we get a
+list of three @code{<mu:message>} objects. Let's just use the
+@code{mu:subject} procedure ('method') provided by @code{<mu:message>} 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{<mu:message>}
+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{<mu:message>}), let's see what we can do with such an object.
+
+@code{<mu:message>} defines the following methods that all take a single
+@code{<mu:message>} 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 "<header-name>")} 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 <message-object> [<contact-type>])}
+
+The @t{<contact-type>} 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{<mu:contact>}) 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{<search-expression>} (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{<mu:contact-with-stats>},
+which is a @emph{subclass} of the @t{<mu:contact>} class discussed in
+@xref{Contact procedures and objects}. @t{<mu:contact-with-stats>} objects
+expose the following additional methods:
+
+@itemize
+@item @code{(mu:frequency <contact>)}: returns the @emph{number of times} this contact occurred in
+one of the address fields
+@item @code{(mu:last-seen <contact>)}: 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
+<mu:contact> 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 <nick> [<name>] "<" <email> ">"
+@end verbatim
+
+@t{mu guile} provides the procedure @code{(mu:contact->string <mu:contact>
+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{<mu-part>} class, and adds two methods to
+@code{<mu:message>} objects:
+@itemize
+@item @code{(mu:parts msg)} - returns a list @code{<mu-part>} 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{<mu:part>} object exposes a few methods to get information about the
+part:
+@itemize
+@item @code{(mu:name <part>)} - returns the file name of the mime-part, or @code{#f} if
+there is none.
+@item @code{(mu:mime-type <part>)} - returns the mime-type of the mime-part, or @code{#f}
+if there is none.
+@item @code{(mu:size <part>)} - 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 <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 <part> <path>)} - 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 <procedure> [<search-expr>])} applies @t{<procedure>} to each
+message matching @t{<search-expr>} (leave empty to match @emph{all} messages),
+and returns a associative list (a list of pairs) with each of the different
+results of @t{<procedure>} 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 <data> <title> <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
--- /dev/null
+/* 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);;
--- /dev/null
+;; 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))
--- /dev/null
+## Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+scmdir=${prefix}/share/guile/site/${GUILE_EFFECTIVE_VERSION}/mu/
+
+scm_DATA= \
+ stats.scm \
+ plot.scm \
+ script.scm
+
+EXTRA_DIST=$(scm_DATA)
--- /dev/null
+* 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:
--- /dev/null
+(define-module (mu contact) :use-module(mu))
+(display "(mu contact) is deprecated, please remove from (use-modules ...)")
+(newline)
+
--- /dev/null
+(define-module (mu message) :use-module (mu))
+(display "(mu message) is deprecated, please remove from (use-modules ...)")
+(newline)
+
--- /dev/null
+(define-module (mu part) :use-module (mu))
+(display "(mu part) is deprecated, please remove from (use-modules ...)")
+(newline)
+
--- /dev/null
+;;
+;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(define-module (mu plot)
+ :use-module (mu)
+ :use-module (ice-9 popen)
+ :export ( mu:plot ;; alias for mu:plot-histogram
+ mu:plot-histogram
+ ))
+
+(define (export-pairs pairs)
+ "Write a temporary file with the list of PAIRS in table format, and
+return the file name."
+ (let* ((datafile (tmpnam))
+ (output (open datafile (logior O_CREAT O_WRONLY) #O0600)))
+ (for-each
+ (lambda(pair)
+ (display (format #f "~a ~a\n" (car pair) (cdr pair)) output))
+ pairs)
+ (close output)
+ datafile))
+
+(define (find-program-in-path prog)
+ "Find exutable program PROG in PATH; return the full path, or #f if
+not found."
+ (let* ((path (parse-path (getenv "PATH")))
+ (progpath (search-path path prog)))
+ (if (not progpath)
+ #f
+ (if (access? progpath X_OK) ;; is
+ progpath
+ #f))))
+
+(define* (mu:plot-histogram data title x-label y-label
+ #:optional (output "dumb") (extra-gnuplot-opts '()))
+ "Plot DATA with TITLE, X-LABEL and X-LABEL using the gnuplot
+program. DATA is a list of cons-pairs (X . Y).
+
+ OUTPUT is a string
+that determines the type of output that gnuplot produces, depending on
+the system. Which options are available depends on the particulars for
+the gnuplot installation, but typical examples would be \"dumb\" for
+text-only display, \"wxterm\" to write to a graphical window, or
+\"png\" to write a PNG-image to stdout.
+
+EXTRA-GNUPLOT-OPTS is a list
+of any additional options for gnuplot."
+ (if (not (find-program-in-path "gnuplot"))
+ (error "cannot find 'gnuplot' in path"))
+ (let ((datafile (export-pairs data))
+ (gnuplot (open-pipe "gnuplot -p" OPEN_WRITE)))
+ (display (string-append
+ "reset\n"
+ "set term " (or output "dumb") "\n"
+ "set title \"" title "\"\n"
+ "set xlabel \"" x-label "\"\n"
+ "set ylabel \"" y-label "\"\n"
+ "set boxwidth 0.9\n"
+ (string-join extra-gnuplot-opts "\n")
+ "plot \"" datafile "\" using 2:xticlabels(1) with boxes fs solid\n")
+ gnuplot)
+ (close-pipe gnuplot)))
+
+;; backward compatibility
+(define mu:plot mu:plot-histogram)
--- /dev/null
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+(define-module (mu script)
+ :export (mu:run-stats))
+
+(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format))
+(use-modules (mu) (mu stats) (mu plot))
+
+(define (help-and-exit)
+ "Show some help."
+ (display
+ (string-append "usage: script [--help] [--textonly] "
+ "[--muhome=<muhome>] [--query=<query>")
+ (newline))
+ (exit 0))
+
+(define (mu:run-stats args func)
+ "Run some statistics function.
+Interpret argument-list ARGS (like command-line
+arguments). Possible arguments are:
+ --help (show some help and exit)
+ --muhome (path to alternative mu home directory)
+ --output (a string describing the output, e.g. \"dumb\", \"png\" \"wxt\")
+ searchexpr (a search query)
+then call FUNC with args SEARCHEXPR and OUTPUT."
+ (setlocale LC_ALL "")
+ (let* ((optionspec '((muhome (value #t))
+ (query (value #t))
+ (output (value #f))
+ (help (single-char #\h) (value #f))))
+ (options (getopt-long args optionspec))
+ (query (option-ref options 'query #f))
+ (help (option-ref options 'help #f))
+ (output (option-ref options 'output #f))
+ (muhome (option-ref options 'muhome #f))
+ (restargs (option-ref options '() #f)))
+ (if help (help-and-exit))
+ (mu:initialize muhome)
+ (func (or query "") output)))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+;;
+;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(define-module (mu stats)
+ :use-module (oop goops)
+ :use-module (mu)
+ :use-module (srfi srfi-1)
+ :use-module (ice-9 i18n)
+ :use-module (ice-9 r5rs)
+ :export ( mu:tabulate
+ mu:top-n-most-frequent
+ mu:count
+ mu:average
+ mu:stddev
+ mu:correl
+ mu:max
+ mu:min
+ mu:weekday-numbers->names
+ mu:month-numbers->names))
+
+
+(define* (mu:tabulate func #:optional (expr #t))
+ "Execute FUNC for each message matching EXPR, and return an alist
+with maps each result of FUNC to its frequency. If the result of FUNC
+is a list, add each of its values separately.
+ FUNC is a function takes a <mu-message> instance as its argument. For
+example, to tabulate messages by weekday, one could use:
+ (mu:tabulate (lambda(msg) (tm:wday (localtime (date msg))))), and
+get back a list like
+ ((1 . 2) (2 . 5)(3 . 4)(4 . 4)(5 . 12)(6 . 7)(7. 2))."
+ (let* ((table '())
+ ;; func to add a value to our table
+ (update-table
+ (lambda (val)
+ (let ((old-freq (or (assoc-ref table val) 0)))
+ (set! table (assoc-set! table val (1+ old-freq)))))))
+ (mu:for-each-message
+ (lambda(msg)
+ (let ((val (func msg)))
+ (if (list? val)
+ (for-each update-table val)
+ (update-table val))))
+ expr)
+ table))
+
+(define* (top-n func less n #:optional (expr #t))
+ "Take the results of (mu:tabulate FUNC EXPR), sort using LESS (a
+function taking two arguments A and B (cons cells, (VAL . FREQ)), and
+returns #t if A < B, #f otherwise), and then take the first N."
+ (take (sort (mu:tabulate func expr) less) n))
+
+(define* (mu:top-n-most-frequent func n #:optional (expr #t))
+ "Take the results of (mu:tabulate FUNC EXPR), and return the N items with the highest frequency."
+ (top-n func (lambda (a b) (> (cdr a) (cdr b))) n expr))
+
+(define* (mu:count #:optional (expr #t))
+ "Count the number of messages matching EXPR. If EXPR is not
+provided, match /all/ messages."
+ (let ((num 0))
+ (mu:for-each-message
+ (lambda (msg) (set! num (1+ num)))
+ expr)
+ num))
+
+(define (average lst)
+ "Calculate the average of a list LST of numbers, or #f if undefined."
+ (if (null? lst)
+ #f
+ (/ (apply + lst) (length lst))))
+
+(define (stddev lst)
+ "Calculate the standard deviation of a list LST of numbers or #f if
+undefined."
+ (let* ((avg (average lst))
+ (sosq (if avg
+ (apply + (map (lambda (x)(* (- x avg) (- x avg))) lst)))))
+ (if sosq
+ (sqrt (/ sosq (length lst))))))
+
+
+(define* (mu:average func #:optional (expr #t))
+ "Get the average value of FUNC applied to all messages matching
+EXPR (or #t for all). Returns #f if undefined."
+ (average (map func (mu:message-list expr))))
+
+(define* (mu:stddev func #:optional (expr #t))
+ "Get the standard deviation the the values of FUNC applied to all
+messages matching EXPR (or #t for all). This is the 'population' stddev, not the 'sample' stddev. Returns #f if undefined."
+ (stddev (map func (mu:message-list expr))))
+
+(define* (mu:max func #:optional (expr #t))
+ "Get the maximum value of FUNC applied to all messages matching
+EXPR (or #t for all). Returns #f if undefined."
+ (apply max (map func (mu:message-list expr))))
+
+(define* (mu:min func #:optional (expr #t))
+ "Get the minimum value of FUNC applied to all messages matching
+EXPR (or #t for all). Returns #f if undefined."
+ (apply min (map func (mu:message-list expr))))
+
+
+(define (correl lst)
+ "Calculate Pearson's correlation coefficient for a list LST of cons
+pair, where the car and cdr of the pairs are values from data set 1
+and 2, respectively."
+ (let ((n (length lst))
+ (sx (apply + (map car lst)))
+ (sy (apply + (map cdr lst)))
+ (sxy (apply + (map (lambda (cell) (* (car cell) (cdr cell))) lst)))
+ (sxx (apply + (map (lambda (cell) (* (car cell) (car cell))) lst)))
+ (syy (apply + (map (lambda (cell) (* (cdr cell) (cdr cell))) lst))))
+ (/ (- (* n sxy) (* sx sy))
+ (sqrt (* (- (* n sxx) (* sx sx)) (- (* n syy) (* sy sy)))))))
+
+(define* (mu:correl func1 func2 #:optional (expr #t))
+ "Determine Pearson's correlation coefficient between the value for
+functions FUNC1 and FUNC2 to all messages matching EXPR (or #t for
+all). Returns #f if undefined."
+ (let ((data
+ (map (lambda (msg)
+ (cons (func1 msg) (func2 msg)))
+ (mu:message-list expr))))
+ (if data (correl data) #f)))
+
+
+;; a list of abbreviated, localized day names
+(define day-names
+ (map locale-day-short (iota 7 1)))
+
+(define (mu:weekday-numbers->names table)
+ "Convert a list of pairs with the car denoting a day number (0-6)
+into a list of pairs with the car replaced by the corresponding day
+name (abbreviated) for the current locale."
+ (map
+ (lambda (pair)
+ (cons (list-ref day-names (car pair)) (cdr pair)))
+ table))
+
+;; a list of abbreviated, localized month names
+(define month-names
+ (map locale-month-short (iota 12 1)))
+
+(define (mu:month-numbers->names table)
+ "Convert a list of pairs with the car denoting a month number (0-11)
+into a list of pairs with the car replaced by the corresponding day
+name (abbreviated)."
+ (map
+ (lambda (pair)
+ (cons (list-ref month-names (car pair)) (cdr pair)))
+ table))
--- /dev/null
+## Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+EXTRA_DIST= \
+ msgs-count.scm \
+ msgs-per-year.scm \
+ msgs-per-hour.scm \
+ msgs-per-month.scm \
+ msgs-per-day.scm \
+ msgs-per-year-month.scm \
+ find-dups.scm
+
+muguiledistscriptdir = $(pkgdatadir)/scripts/
+muguiledistscript_SCRIPTS = $(EXTRA_DIST)
--- /dev/null
+#!/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:
+
+
+
+
+
+
+
+
+
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO: --query=<query>: limit to messages matching query
+;; INFO: --muhome=<muhome>: path to mu home dir
+
+(use-modules (mu) (mu script) (mu stats))
+
+(define (count expr output)
+ "Print the total number of messages matching the query EXPR.
+OUTPUT is ignored."
+ (display (mu:count expr))
+ (newline))
+
+(define (main args)
+ (mu:run-stats args count))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO: --query=<query>: limit to messages matching query
+;; INFO: --muhome=<muhome>: path to mu home dir
+;; INFO: --output: the output format, such as "png", "wxt"
+;; INFO: (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-day expr output)
+ "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (mu:weekday-numbers->names
+ (sort (mu:tabulate
+ (lambda (msg)
+ (tm:wday (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y)))))
+ (format #f "Messages per weekday matching ~a" expr)
+ "Day" "Messages" output))
+
+(define (main args)
+ (mu:run-stats args per-day))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO: --query=<query>: limit to messages matching query
+;; INFO: --muhome=<muhome>: path to mu home dir
+;; INFO: --output: the output format, such as "png", "wxt"
+;; INFO: (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-hour expr output)
+ "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (sort
+ (mu:tabulate
+ (lambda (msg)
+ (tm:hour (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per hour matching ~a" expr)
+ "Hour" "Messages" output))
+
+(define (main args)
+ (mu:run-stats args per-hour))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO: --query=<query>: limit to messages matching query
+;; INFO: --muhome=<muhome>: path to mu home dir
+;; INFO: --output: the output format, such as "png", "wxt"
+;; INFO: (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-month expr output)
+ "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (mu:month-numbers->names
+ (sort
+ (mu:tabulate
+ (lambda (msg)
+ (tm:mon (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y)))))
+ (format #f "Messages per month matching ~a" expr)
+ "Month" "Messages" output))
+
+(define (main args)
+ (mu:run-stats args per-month))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO: --query=<query>: limit to messages matching query
+;; INFO: --muhome=<muhome>: path to mu home dir
+;; INFO: --output: the output format, such as "png", "wxt"
+;; INFO: (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-year-month expr output)
+ "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (sort (mu:tabulate
+ (lambda (msg)
+ (string->number
+ (format #f "~d~2'0d"
+ (+ 1900 (tm:year (localtime (mu:date msg))))
+ (tm:mon (localtime (mu:date msg))))))
+ expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per year/month matching ~a" expr)
+ "Year/Month" "Messages" output))
+
+(define (main args)
+ (mu:run-stats args per-year-month))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO: --query=<query>: limit to messages matching query
+;; INFO: --muhome=<muhome>: path to mu home dir
+;; INFO: --output: the output format, such as "png", "wxt"
+;; INFO: (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-year expr output)
+ "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (sort (mu:tabulate
+ (lambda (msg)
+ (+ 1900 (tm:year (localtime (mu:date msg))))) expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per year matching ~a" expr)
+ "Year" "Messages" output))
+
+(define (main args)
+ (mu:run-stats args per-year))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+## 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.
+
+
+guile_load_path=':'.join([ # meson 0.56 has project_source_root
+ join_paths(meson.source_root(), 'guile'),
+ join_paths(meson.current_build_dir(), '..')])
+
+test('test-mu-guile',
+ executable('test-mu-guile',
+ 'test-mu-guile.cc',
+ install: false,
+ cpp_args: [
+ '-DABS_SRCDIR="' + meson.current_source_dir() + '"',
+ '-DGUILE_LOAD_PATH="' + guile_load_path + '"',
+ '-DGUILE_EXTENSIONS_PATH="' + guile_load_path + '"'
+ ],
+ dependencies: [glib_dep, lib_mu_dep]))
--- /dev/null
+/*
+** 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;
+}
--- /dev/null
+#!/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:
--- /dev/null
+## Copyright (C) 2010-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.
+
+# enforce compiling guile (optionally) first,then this dir first
+# before descending into tests/
+include $(top_srcdir)/gtest.mk
+
+SUBDIRS= thirdparty utils message index
+
+TESTDEFS= \
+ -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\" \
+ -DMU_TESTMAILDIR2=\"${abs_srcdir}/testdir2\" \
+ -DMU_TESTMAILDIR4=\"${abs_srcdir}/testdir4\" \
+ -DABS_CURDIR=\"${abs_builddir}\" \
+ -DABS_SRCDIR=\"${abs_srcdir}\"
+
+
+AM_CFLAGS= \
+ $(WARN_CFLAGS) \
+ $(GMIME_CFLAGS) \
+ $(XAPIAN_CFLAGS) \
+ $(GLIB_CFLAGS) \
+ $(GUILE_CFLAGS) \
+ $(ASAN_CFLAGS) \
+ $(CODE_COVERAGE_CFLAGS) \
+ $(TESTDEFS) \
+ -Wno-format-nonliteral \
+ -Wno-switch-enum \
+ -Wno-deprecated-declarations \
+ -Wno-inline
+
+AM_CXXFLAGS= \
+ $(GMIME_CFLAGS) \
+ $(GLIB_CFLAGS) \
+ $(GUILE_CFLAGS) \
+ $(WARN_CXXFLAGS) \
+ $(XAPIAN_CXXFLAGS) \
+ $(ASAN_CXXFLAGS) \
+ $(CODE_COVERAGE_CFLAGS) \
+ $(TESTDEFS)
+
+AM_CPPFLAGS= \
+ $(CODE_COVERAGE_CPPFLAGS)
+
+noinst_LTLIBRARIES= \
+ libmu.la
+
+libmu_la_SOURCES= \
+ mu-bookmarks.cc \
+ mu-bookmarks.hh \
+ mu-contacts-cache.cc \
+ mu-contacts-cache.hh \
+ mu-parser.cc \
+ mu-parser.hh \
+ mu-query.cc \
+ mu-query.hh \
+ mu-query-results.hh \
+ mu-query-match-deciders.cc \
+ mu-query-match-deciders.hh \
+ mu-query-threads.cc \
+ mu-query-threads.hh \
+ mu-runtime.cc \
+ mu-runtime.hh \
+ mu-script.cc \
+ mu-script.hh \
+ mu-server.cc \
+ mu-server.hh \
+ mu-store.cc \
+ mu-store.hh \
+ mu-tokenizer.cc \
+ mu-tokenizer.hh \
+ mu-tree.hh \
+ mu-xapian.cc \
+ mu-xapian.hh \
+ mu-maildir.cc \
+ mu-maildir.hh
+
+libmu_la_LIBADD= \
+ $(XAPIAN_LIBS) \
+ $(GMIME_LIBS) \
+ $(GLIB_LIBS) \
+ $(GUILE_LIBS) \
+ ${builddir}/message/libmu-message.la \
+ ${builddir}/index/libmu-index.la \
+ $(CODE_COVERAGE_LIBS)
+
+libmu_la_LDFLAGS= \
+ $(ASAN_LDFLAGS)
+
+noinst_PROGRAMS= \
+ tokenize
+
+tokenize_SOURCES= \
+ tokenize.cc
+
+tokenize_LDADD= \
+ $(WARN_LDFLAGS) \
+ libmu.la \
+ utils/libmu-utils.la
+
+EXTRA_DIST= \
+ doxyfile.in
+
+CLEANFILES=*.log *.trs *core* *vgdump* *.gcda *.gcno
+
+include $(top_srcdir)/aminclude_static.am
--- /dev/null
+# 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
--- /dev/null
+## 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 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+AM_CPPFLAGS= \
+ $(CODE_COVERAGE_CPPFLAGS)
+
+AM_CXXFLAGS= \
+ $(WARN_CXXFLAGS) \
+ $(GLIB_CFLAGS) \
+ $(XAPIAN_CFLAGS) \
+ $(ASAN_CXXFLAGS) \
+ $(CODE_COVERAGE_CFLAGS) \
+ -I${top_srcdir}/lib
+
+AM_LDFLAGS= \
+ $(ASAN_LDFLAGS)
+
+noinst_LTLIBRARIES= \
+ libmu-index.la
+
+libmu_index_la_SOURCES= \
+ mu-indexer.cc \
+ mu-indexer.hh \
+ mu-scanner.cc \
+ mu-scanner.hh
+
+libmu_index_la_LIBADD= \
+ $(GLIB_LIBS) \
+ $(CODE_COVERAGE_LIBS)
+
+include $(top_srcdir)/aminclude_static.am
--- /dev/null
+## 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_pkgconfig_variable('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
+)
--- /dev/null
+/*
+** 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_;
+}
--- /dev/null
+/*
+** 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__ */
--- /dev/null
+/*
+** Copyright (C) 2020-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.
+**
+*/
+#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-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_special_dir(const char* d_name)
+{
+ return d_name[0] == '\0' || (d_name[1] == '\0' && d_name[0] == '.') ||
+ (d_name[2] == '\0' && d_name[0] == '.' && d_name[1] == '.');
+}
+
+bool
+Scanner::Private::process_dentry(const std::string& path, struct dirent* dentry,
+ bool is_maildir)
+{
+ const auto d_name{dentry->d_name};
+
+ if (is_special_dir(d_name) || std::strcmp(d_name, "tmp") == 0)
+ return true; // ignore.
+
+ const auto fullpath{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_;
+}
--- /dev/null
+/*
+** 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__ */
--- /dev/null
+/*
+** 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;
+}
--- /dev/null
+## 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-runtime.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')
--- /dev/null
+## 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.
+include $(top_srcdir)/gtest.mk
+
+AM_CXXFLAGS= \
+ $(WARN_CXXFLAGS) \
+ $(GLIB_CFLAGS) \
+ $(GMIME_CFLAGS) \
+ $(XAPIAN_CFLAGS) \
+ -I${top_srcdir}/lib
+
+noinst_LTLIBRARIES= \
+ libmu-message.la
+
+libmu_message_la_SOURCES= \
+ mu-message.cc \
+ mu-message.hh \
+ mu-message-file.cc \
+ mu-message-file.hh \
+ mu-message-part.cc \
+ mu-message-part.hh \
+ mu-contact.hh \
+ mu-contact.cc \
+ mu-document.cc \
+ mu-document.hh \
+ mu-fields.hh \
+ mu-fields.cc \
+ mu-flags.hh \
+ mu-flags.cc \
+ mu-priority.hh \
+ mu-priority.cc \
+ mu-mime-object.cc \
+ mu-mime-object.hh
+
+libmu_message_la_LIBADD= \
+ $(GLIB_LIBS) \
+ $(GMIME_LIBS) \
+ $(XAPIAN_LIBS)
--- /dev/null
+## 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]))
--- /dev/null
+/*
+** 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-contact.hh"
+#include "mu-message.hh"
+#include "utils/mu-utils.hh"
+#include "mu-mime-object.hh"
+
+#include <gmime/gmime.h>
+#include <glib.h>
+#include <string>
+
+using namespace Mu;
+
+static bool
+needs_quoting(const std::string& name)
+{
+ for (auto& c: name)
+ if (c == ',' || c == '"')
+ return true;
+ return false;
+}
+
+std::string
+Contact::display_name(bool quote) const
+{
+
+ if (name.empty())
+ return email;
+ else if (!quote || !needs_quoting(name))
+ return name + " <" + email + '>';
+ else
+ return address_rfc2047(*this);
+}
+
+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.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.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_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_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(true),
+ "\"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_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));
+}
+
+
+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);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** 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; basically, if there's a
+ * non-empty name, it's
+ * Jane Doe <email@example.com>
+ * otherwise it's just the e-mail address.
+ *
+ * @param quote_if_needed if true, handle quoting of the name-part as well. This
+ * is useful when the address is to be used directly in emails.
+ *
+ * @return the display name
+ */
+ std::string display_name(bool quote_if_needed=false) 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__ */
--- /dev/null
+/*
+** 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 <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;
+
+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");
+}
+
+
+static std::string
+make_prop_name(const Field& field)
+{
+ return ":" + std::string(field.name);
+}
+
+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);
+
+ if (field.include_in_sexp())
+ sexp_list().add_prop(make_prop_name(field),
+ Sexp::make_string(std::move(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::List elms;
+ for(auto&& val: vals)
+ elms.add(Sexp::make_string(val));
+ sexp_list().add_prop(make_prop_name(field),
+ Sexp::make_list(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::List clist;
+
+ seq_for_each(contacts, [&](auto&& c) {
+ if (!c.name.empty())
+ clist.add(Sexp::make_prop_list(
+ ":name", Sexp::make_string(c.name),
+ ":email", Sexp::make_string(c.email)));
+ else
+ clist.add(Sexp::make_prop_list(
+ ":email", Sexp::make_string(c.email)));
+ });
+
+ return Sexp::make_list(std::move(clist));
+}
+
+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())
+ sexp_list().add_prop(make_prop_name(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())
+ sexp_list().add_prop(std::string{propname},
+ make_contacts_sexp(contacts));
+}
+
+
+static Sexp
+make_emacs_time_sexp(::time_t t)
+{
+ Sexp::List dlist;
+
+ dlist.add(Sexp::make_number(static_cast<unsigned>(t >> 16)));
+ dlist.add(Sexp::make_number(static_cast<unsigned>(t & 0xffff)));
+ dlist.add(Sexp::make_number(0));
+
+ return Sexp::make_list(std::move(dlist));
+}
+
+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())
+ sexp_list().add_prop(make_prop_name(field),
+ make_emacs_time_sexp(val));
+ else
+ sexp_list().add_prop(make_prop_name(field),
+ Sexp::make_number(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())
+ sexp_list().add_prop(make_prop_name(field),
+ Sexp::make_symbol_sv(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::List 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::make_symbol_sv(flag_info.name));
+ }
+ });
+
+ if (field.include_in_sexp())
+ sexp_list().add_prop(make_prop_name(field),
+ Sexp::make_list(std::move(flaglist)));
+}
+
+
+Sexp::List&
+Document::sexp_list()
+{
+ /* perhaps we need get the sexp_ from the document first? */
+ if (sexp_list_.empty()) {
+ const auto str{xdoc_.get_data()};
+ if (!str.empty()) {
+ Sexp sexp{Sexp::make_parse(str)};
+ sexp_list_ = sexp.list();
+ }
+ }
+
+ return sexp_list_;
+}
+
+std::string
+Document::cached_sexp() const
+{
+ return xdoc_.get_data();
+}
+
+void
+Document::update_cached_sexp(void)
+{
+ if (sexp_list_.empty())
+ return; /* nothing to do; i.e. the exisiting sexp is still up to
+ * date */
+ xdoc_.set_data(Sexp::make_list(Sexp::List{sexp_list()}).to_sexp_string());
+}
+
+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*/
--- /dev/null
+/** 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} {}
+
+ /**
+ * Get a reference to the underlying Xapian document.
+ *
+ */
+ const Xapian::Document& xapian_document() const { return xdoc_; }
+
+ /**
+ * 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);
+
+ /**
+ * Update the cached sexp from the sexp_list_
+ */
+ void update_cached_sexp();
+
+ /**
+ * Get the cached s-expression
+ *
+ * @return a string
+ */
+ std::string cached_sexp() const;
+
+ /**
+ * Get the cached s-expressionl useful for changing
+ * it (call update_sexp_cache() when done)
+ *
+ * @return the cache s-expression
+ */
+ Sexp::List& sexp_list();
+
+ /**
+ * 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:
+ Xapian::Document xdoc_;
+ Sexp::List sexp_list_;
+
+};
+
+} // namepace Mu
+
+#endif /* MU_DOCUMENT_HH__ */
--- /dev/null
+/*
+** 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*/
--- /dev/null
+/*
+** 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_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__ */
--- /dev/null
+/*
+** 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));
+
+ static_assert(!flags_from_absolute_expr("DRT?"));
+ static_assert(flags_from_absolute_expr("DRT?", true/*ignore invalid*/).value() ==
+ (Flags::Draft | Flags::Replied |
+ Flags::Trashed));
+ 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);
+ 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_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));
+}
+
+
+#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);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** 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_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;
+}
+
+/**
+ * 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 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 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;
+}
+
+/**
+ * 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__ */
--- /dev/null
+/*
+** 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{ G_DIR_SEPARATOR_S "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*/
--- /dev/null
+/*
+** 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__ */
--- /dev/null
+/*
+** 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-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;
+}
+
+Option<std::string>
+MessagePart::cooked_filename() const noexcept
+{
+ // make a bit more pallatble.
+ auto cleanup = [](const std::string& name)->std::string {
+ std::string clean;
+ clean.reserve(name.length());
+ for (auto& c: name) {
+ auto taboo{(::iscntrl(c) || c == G_DIR_SEPARATOR ||
+ c == ' ' || c == '\\' || c == ':')};
+ clean += (taboo ? '-' : c);
+ }
+ if (clean.size() > 1 && clean[0] == '-')
+ clean.erase(0, 1);
+
+ return clean;
+ };
+
+ // a MimePart... use the name if there is one.
+ if (mime_object().is_part())
+ return MimePart{mime_object()}.filename().map(cleanup);
+
+ // 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(cleanup)
+ .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 Err(Error::Code::InvalidArgument,
+ "not a part");
+ else
+ return MimePart{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();
+}
--- /dev/null
+/*
+** 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_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 MimeMessagePart).
+ *
+ * @see raw_filename()
+ *
+ * @return the name
+ */
+ Option<std::string> cooked_filename() 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__ */
--- /dev/null
+/*
+** 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-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-util.h>
+#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)
+{
+ if (!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)};
+ 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::List&
+Message::to_sexp_list() const
+{
+ return priv_->doc.sexp_list();
+}
+
+void
+Message::update_cached_sexp()
+{
+ priv_->doc.update_cached_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", ' '}
+ }};
+ static const auto strip_rx{std::regex("^\\s+| +$|( )\\s+")};
+
+ 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)) {
+ tags.emplace_back(
+ std::regex_replace(tagval, strip_rx, "$1"));
+ }
+ }
+ });
+
+ 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());
+ 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();
+}
--- /dev/null
+/*
+** 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 "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) */
+ };
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Get the cached s-expression for this message, or {} if not available.
+ *
+ * @return sexp or empty.
+ */
+ std::string cached_sexp() const {
+ return document().cached_sexp();
+ }
+
+ /*
+ * Convert to Sexp
+ */
+
+ /**
+ * Get the s-expression for this message. Stays valid as long
+ * as this message is.
+ *
+ * @return a Mu::Sexp::List representing the message.
+ */
+ const Mu::Sexp::List& to_sexp_list() const;
+ Mu::Sexp to_sexp() const {
+ return Sexp::make_list(Sexp::List(to_sexp_list()));
+ }
+
+ /**
+ * Update the cached sexp for this message which is stored in the
+ * document. This should be done immediately before storing it in the
+ * database.
+ *
+ */
+ void update_cached_sexp();
+
+ /*
+ * 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);
+
+} // Mu
+#endif /* MU_MESSAGE_HH__ */
--- /dev/null
+/*
+** 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 <errno.h>
+
+using namespace Mu;
+
+
+\f
+/* 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;
+}
+
+\f
+/*
+ * 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));
+}
+
+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;
+}
+
+\f
+/*
+ * 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();
+}
+
+\f
+/*
+ * 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);
+}
+
+
+\f
+/*
+ * 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));
+}
--- /dev/null
+/*
+** 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_{};
+};
+
+
+
+
+
+\f
+/**
+ * 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());
+ }
+};
+
+
+
+
+\f
+/**
+ * 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;
+}
+
+\f
+/**
+ * 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());
+ }
+};
+
+
+\f
+/**
+ * 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);
+}
+
+
+\f
+/**
+ * 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;
+}
+
+
+
+\f
+/**
+* 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);
+}
+
+\f
+/**
+ * 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());
+ }
+};
+
+\f
+/**
+ * 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;
+
+ /*
+ * 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());
+ }
+};
+
+\f
+/**
+ * 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());
+ }
+};
+\f
+/**
+ * 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 \18bqexists
+ *
+ * @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());
+ }
+};
+
+
+\f
+/**
+ * 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());
+ }
+
+};
+\f/**
+ * 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());
+ }
+};
+
+\f
+/**
+ * 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:
+
+ Option<MimePart> part(int index) const {
+ if (MimeObject mobj{g_mime_multipart_get_part(self(),index)}; !mobj)
+ return Nothing;
+ else
+ return mobj;
+ }
+
+ GMimeMultipart* self() const {
+ return reinterpret_cast<GMimeMultipart*>(object());
+ }
+};
+
+\f
+/**
+ * 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);
+
+\f
+/**
+ * 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__ */
--- /dev/null
+/*
+** 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*/
--- /dev/null
+/*
+** 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_*/
--- /dev/null
+/*
+** 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-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(), "tmp-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");
+ }
+
+ {
+ 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->cached_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>
+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());
+
+ 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_true(message->cc().empty());
+ 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));
+}
+
+
+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 */
+ g_assert_true(g_str_has_suffix(m2->message_id().c_str(), "@mu.id"));
+ g_assert_true(g_str_has_suffix(m3->message_id().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");
+}
+
+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/fail",
+ test_message_fail);
+ g_test_add_func("/message/message/sanitize-maildir",
+ test_message_sanitize_maildir);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** 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);
+}
--- /dev/null
+/*
+** 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__*/
--- /dev/null
+/*
+** 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 <regex>
+#include <ctime>
+
+#include <utils/mu-utils.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<std::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<std::regex> make_personal_rx(const StringVec& personal) const {
+ std::vector<std::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)};
+ rxvec.emplace_back(std::regex(
+ rxstr, std::regex::basic | std::regex::optimize |
+ std::regex::icase));
+ } catch (const std::regex_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() != 6)) {
+ g_warning("error: '%s'", line.c_str());
+ continue;
+ }
+ Contact ci(parts[1], // email
+ std::move(parts[2]), // name
+ (time_t)g_ascii_strtoll(parts[4].c_str(), NULL, 10), // message_date
+ parts[3][0] == '1' ? true : false, // personal
+ (std::size_t)g_ascii_strtoll(parts[5].c_str(), NULL, 10), // frequency
+ g_get_monotonic_time()); // tstamp
+ contacts.emplace(std::move(parts[1]), 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"
+ "%s%s"
+ "%d%s"
+ "%" G_GINT64_FORMAT "%s"
+ "%" G_GINT64_FORMAT "\n",
+ ci.display_name().c_str(),
+ Separator,
+ 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_;
+}
+
+//const Contact
+void
+ContactsCache::add(Contact&& contact)
+{
+ 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;
+
+ 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;
+ }
+ }
+}
+
+
+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_) {
+ std::smatch m; // perhaps cache addr in personal_plain_?
+ if (std::regex_match(addr, m, rx))
+ 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*/
--- /dev/null
+/*
+** 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
+ *
+ * @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"
+ *
+ * @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__ */
--- /dev/null
+/*
+** 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-util.h"
+
+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 mu_util_get_dtype(path.c_str(), 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{path + G_DIR_SEPARATOR_S + subdir};
+
+ /* if subdir already exists, don't try to re-create
+ * it */
+ if (mu_util_check_dir(fullpath.c_str(), TRUE, TRUE))
+ 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 || !mu_util_check_dir(fullpath.c_str(), TRUE, TRUE))
+ 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{path + G_DIR_SEPARATOR_S 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()));
+
+ char *srcfile{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 = format("%s%c%s%c%u_%s",
+ targetpath.c_str(),
+ G_DIR_SEPARATOR, in_cur ? "cur" : "new",
+ G_DIR_SEPARATOR,
+ g_str_hash(src.c_str()),
+ srcfile);
+ else
+ fulltargetpath = format("%s%c%s%c%s",
+ targetpath.c_str(),
+ G_DIR_SEPARATOR, in_cur ? "cur" : "new",
+ G_DIR_SEPARATOR,
+ srcfile);
+ g_free(srcfile);
+
+ 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{
+ format("%s" G_DIR_SEPARATOR_S "%s",path.c_str(), 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 :
+ 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 dst_mdir + G_DIR_SEPARATOR_S + subdir + G_DIR_SEPARATOR_S + dst_file;
+}
--- /dev/null
+/*
+** 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__*/
--- /dev/null
+/*
+** 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 <regex>
+#include <limits>
+
+#include "mu-tokenizer.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-error.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 std::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 std::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 (std::regex_search(val, rx))
+ 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 = std::regex(rxstr);
+ for (const auto& field : fields) {
+ const auto terms = process_regex(field.field, rx);
+ for (const auto& term : terms) {
+ tree.add_child(Tree({Node::Type::Value,
+ 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();
+ }
+}
--- /dev/null
+/*
+** 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__ */
--- /dev/null
+/*
+** 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);
+}
--- /dev/null
+/*
+** 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__ */
--- /dev/null
+/*
+** 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__ */
--- /dev/null
+/*
+** 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.
+**
+*/
+
+#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_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_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/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*/
--- /dev/null
+/*
+** 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__*/
--- /dev/null
+/*
+** 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*/
--- /dev/null
+/*
+** 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__*/
--- /dev/null
+/*
+** Copyright (C) 2019-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 "mu-runtime.hh"
+#include "utils/mu-util.h"
+#include "utils/mu-logger.hh"
+
+#include <locale.h> /* for setlocale() */
+
+#include <string>
+#include <unordered_map>
+static std::unordered_map<MuRuntimePath, std::string> RuntimePaths;
+
+constexpr auto PartsDir = "parts";
+constexpr auto LogDir = "log";
+constexpr auto XapianDir = "xapian";
+constexpr auto MuName = "mu";
+constexpr auto Bookmarks = "bookmarks";
+
+static const std::string Sepa{G_DIR_SEPARATOR_S};
+
+static void
+init_paths_xdg()
+{
+ RuntimePaths.emplace(MU_RUNTIME_PATH_XAPIANDB,
+ g_get_user_cache_dir() + Sepa + MuName + Sepa + XapianDir);
+ RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, g_get_user_cache_dir() + Sepa + MuName);
+ RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE,
+ g_get_user_cache_dir() + Sepa + MuName + Sepa + PartsDir);
+ RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, g_get_user_cache_dir() + Sepa + MuName);
+ RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, g_get_user_config_dir() + Sepa + MuName);
+}
+
+static void
+init_paths_muhome(const char* muhome)
+{
+ RuntimePaths.emplace(MU_RUNTIME_PATH_XAPIANDB, muhome + Sepa + XapianDir);
+ RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, muhome);
+ RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE, muhome + Sepa + PartsDir);
+ RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, muhome + Sepa + LogDir);
+ RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, muhome + Sepa + Bookmarks);
+}
+
+gboolean
+mu_runtime_init(const char* muhome, const char* name, gboolean debug)
+{
+ g_return_val_if_fail(RuntimePaths.empty(), FALSE);
+ g_return_val_if_fail(name, FALSE);
+
+ setlocale(LC_ALL, "");
+
+ if (muhome)
+ init_paths_muhome(muhome);
+ else
+ init_paths_xdg();
+
+ for (const auto& d : RuntimePaths) {
+ char* dir;
+ if (d.first == MU_RUNTIME_PATH_BOOKMARKS) // special case
+ dir = g_path_get_dirname(d.second.c_str());
+ else
+ dir = g_strdup(d.second.c_str());
+
+ auto ok = mu_util_create_dir_maybe(dir, 0700, TRUE);
+ if (!ok) {
+ g_critical("failed to create %s", dir);
+ g_free(dir);
+ mu_runtime_uninit();
+ return FALSE;
+ }
+ g_free(dir);
+ }
+
+ const auto log_path = RuntimePaths[MU_RUNTIME_PATH_LOGDIR] + Sepa + name + ".log";
+
+ using namespace Mu;
+ LogOptions opts{LogOptions::None};
+ if (debug)
+ opts |= (LogOptions::Debug | LogOptions::None);
+
+ Mu::log_init(log_path, opts);
+
+ return TRUE;
+}
+
+void
+mu_runtime_uninit(void)
+{
+ RuntimePaths.clear();
+ Mu::log_uninit();
+}
+
+const char*
+mu_runtime_path(MuRuntimePath path)
+{
+ const auto it = RuntimePaths.find(path);
+ if (it == RuntimePaths.end())
+ return NULL;
+ else
+ return it->second.c_str();
+}
--- /dev/null
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+**
+** Copyright (C) 2012-2019 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#ifndef __MU_RUNTIME_H__
+#define __MU_RUNTIME_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+/**
+ * initialize the mu runtime system; initializes logging and other
+ * systems. To uninitialize, use mu_runtime_uninit
+ *
+ * @param muhome path where to find the mu home directory (typically, ~/.cache/mu)
+ * @param name of the main program, ie. 'mu', 'mug' or
+ * 'procmule'. this influences the name of the e.g. the logfile
+ * @param debug debug-mode
+ *
+ * @return TRUE if succeeded, FALSE in case of error
+ */
+gboolean mu_runtime_init(const char* muhome, const char* name, gboolean debug);
+
+/**
+ * free all resources
+ *
+ */
+void mu_runtime_uninit(void);
+
+typedef enum {
+ MU_RUNTIME_PATH_XAPIANDB, /* mu xapian db path */
+ MU_RUNTIME_PATH_BOOKMARKS, /* mu bookmarks file path */
+ MU_RUNTIME_PATH_CACHE, /* mu cache path */
+ MU_RUNTIME_PATH_MIMECACHE, /* mu cache path for attachments etc. */
+ MU_RUNTIME_PATH_LOGDIR, /* mu path for log files */
+
+ MU_RUNTIME_PATH_NUM
+} MuRuntimePath;
+
+/**
+ * get a file system path to some 'special' file or directory
+ *
+ * @return ma string which should be not be modified/freed, or NULL in
+ * case of error.
+ */
+const char* mu_runtime_path(MuRuntimePath path);
+
+G_END_DECLS
+
+#endif /*__MU_RUNTIME_H__*/
--- /dev/null
+/*
+** Copyright (C) 2012-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.
+**
+*/
+
+#include "config.h"
+
+#ifdef BUILD_GUILE
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+#endif /*BUILD_GUILE*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <errno.h>
+#include <unistd.h>
+
+#include "mu-script.hh"
+#include "utils/mu-util.h"
+
+/**
+ * Structure with information about a certain script.
+ * the values will be *freed* when MuScriptInfo is freed
+ */
+struct MuScriptInfo {
+ char* _name; /* filename-sans-extension */
+ char* _path; /* full path to script */
+ char* _oneline; /* one-line description */
+ char* _descr; /* longer description */
+};
+
+/* create a new MuScriptInfo* object*/
+static MuScriptInfo*
+script_info_new(void)
+{
+ return g_slice_new0(MuScriptInfo);
+}
+
+/* destroy a MuScriptInfo* object */
+static void
+script_info_destroy(MuScriptInfo* msi)
+{
+ if (!msi)
+ return;
+
+ g_free(msi->_name);
+ g_free(msi->_path);
+ g_free(msi->_oneline);
+ g_free(msi->_descr);
+
+ g_slice_free(MuScriptInfo, msi);
+}
+
+/* compare two MuScripInfo* objects (for sorting) */
+static int
+script_info_cmp(MuScriptInfo* msi1, MuScriptInfo* msi2)
+{
+ return strcmp(msi1->_name, msi2->_name);
+}
+
+const char*
+mu_script_info_name(MuScriptInfo* msi)
+{
+ g_return_val_if_fail(msi, NULL);
+ return msi->_name;
+}
+
+const char*
+mu_script_info_path(MuScriptInfo* msi)
+{
+ g_return_val_if_fail(msi, NULL);
+ return msi->_path;
+}
+
+const char*
+mu_script_info_one_line(MuScriptInfo* msi)
+{
+ g_return_val_if_fail(msi, NULL);
+ return msi->_oneline;
+}
+
+const char*
+mu_script_info_description(MuScriptInfo* msi)
+{
+ g_return_val_if_fail(msi, NULL);
+ return msi->_descr;
+}
+
+gboolean
+mu_script_info_matches_regex(MuScriptInfo* msi, const char* rxstr, GError** err)
+{
+ GRegex* rx;
+ gboolean match;
+
+ g_return_val_if_fail(msi, FALSE);
+ g_return_val_if_fail(rxstr, FALSE);
+
+ rx = g_regex_new(rxstr,
+ (GRegexCompileFlags)(G_REGEX_CASELESS | G_REGEX_OPTIMIZE),
+ (GRegexMatchFlags)0,
+ err);
+ if (!rx)
+ return FALSE;
+
+ match = FALSE;
+ if (msi->_name)
+ match = g_regex_match(rx, msi->_name, (GRegexMatchFlags)0, NULL);
+ if (!match && msi->_oneline)
+ match = g_regex_match(rx, msi->_oneline, (GRegexMatchFlags)0, NULL);
+
+ return match;
+}
+
+void
+mu_script_info_list_destroy(GSList* lst)
+{
+ g_slist_free_full(lst, (GDestroyNotify)script_info_destroy);
+}
+
+static GIOChannel*
+open_channel(const char* path)
+{
+ GError* err;
+ GIOChannel* io_chan;
+
+ err = NULL;
+
+ io_chan = g_io_channel_new_file(path, "r", &err);
+ if (!io_chan) {
+ g_warning("failed to open '%s': %s",
+ path,
+ err ? err->message : "something went wrong");
+ g_clear_error(&err);
+ return NULL;
+ }
+
+ return io_chan;
+}
+
+static void
+end_channel(GIOChannel* io_chan)
+{
+ GIOStatus status;
+ GError* err;
+
+ err = NULL;
+ status = g_io_channel_shutdown(io_chan, FALSE, &err);
+ if (status != G_IO_STATUS_NORMAL) {
+ g_warning("failed to shutdown io-channel: %s",
+ err ? err->message : "something went wrong");
+ g_clear_error(&err);
+ }
+
+ g_io_channel_unref(io_chan);
+}
+
+static gboolean
+get_descriptions(MuScriptInfo* msi, const char* prefix)
+{
+ GIOStatus io_status;
+ GIOChannel* script_io;
+ GError* err;
+
+ char *line, *descr, *oneline;
+
+ if (!prefix)
+ return TRUE; /* not an error */
+
+ if (!(script_io = open_channel(msi->_path)))
+ return FALSE;
+
+ err = NULL;
+ line = descr = oneline = NULL;
+
+ do {
+ g_free(line);
+ io_status = g_io_channel_read_line(script_io, &line, NULL, NULL, &err);
+ if (io_status != G_IO_STATUS_NORMAL)
+ break;
+
+ if (!g_str_has_prefix(line, prefix))
+ continue;
+
+ if (!oneline)
+ oneline = g_strdup(line + strlen(prefix));
+ else {
+ char* tmp;
+ tmp = descr;
+ descr = g_strdup_printf("%s%s", descr ? descr : "", line + strlen(prefix));
+ g_free(tmp);
+ }
+
+ } while (TRUE);
+
+ if (io_status != G_IO_STATUS_EOF) {
+ g_warning("error reading %s: %s",
+ msi->_path,
+ err ? err->message : "something went wrong");
+ g_clear_error(&err);
+ }
+
+ end_channel(script_io);
+ msi->_oneline = oneline;
+ msi->_descr = descr;
+
+ return TRUE;
+}
+
+GSList*
+mu_script_get_script_info_list(const char* path,
+ const char* ext,
+ const char* descprefix,
+ GError** err)
+{
+ DIR* dir;
+ GSList* lst;
+ struct dirent* dentry;
+
+ g_return_val_if_fail(path, NULL);
+
+ dir = opendir(path);
+ if (!dir) {
+ mu_util_g_set_error(err,
+ MU_ERROR_FILE_CANNOT_OPEN,
+ "failed to open '%s': %s",
+ path,
+ g_strerror(errno));
+ return NULL;
+ }
+
+ /* create a list of names, paths */
+ lst = NULL;
+ while ((dentry = readdir(dir))) {
+ MuScriptInfo* msi;
+ /* only consider files with certain extensions,
+ * if ext != NULL */
+ if (ext && !g_str_has_suffix(dentry->d_name, ext))
+ continue;
+ msi = script_info_new();
+ msi->_name = g_strdup(dentry->d_name);
+ if (ext) /* strip the extension */
+ msi->_name[strlen(msi->_name) - strlen(ext)] = '\0';
+ msi->_path = g_strdup_printf("%s%c%s", path, G_DIR_SEPARATOR, dentry->d_name);
+ /* set the one-line and long description */
+ get_descriptions(msi, descprefix);
+ lst = g_slist_prepend(lst, msi);
+ }
+
+ closedir(dir); /* ignore error checking... */
+
+ return g_slist_sort(lst, (GCompareFunc)script_info_cmp);
+}
+
+MuScriptInfo*
+mu_script_find_script_with_name(GSList* lst, const char* name)
+{
+ GSList* cur;
+
+ g_return_val_if_fail(name, NULL);
+
+ for (cur = lst; cur; cur = g_slist_next(cur)) {
+ MuScriptInfo* msi;
+ msi = (MuScriptInfo*)cur->data;
+
+ if (g_strcmp0(name, mu_script_info_name(msi)) == 0)
+ return msi;
+ }
+
+ return NULL;
+}
+
+
+
+#ifdef BUILD_GUILE
+static char*
+quoted_from_strv (const gchar **params)
+{
+ GString *str;
+ int i;
+
+ g_return_val_if_fail (params, NULL);
+
+ if (!params[0])
+ return g_strdup ("");
+
+ str = g_string_sized_new (64); /* just a guess */
+
+ for (i = 0; params[i]; ++i) {
+
+ if (i > 0)
+ g_string_append_c (str, ' ');
+
+ g_string_append_c (str, '"');
+ g_string_append (str, params[i]);
+ g_string_append_c (str, '"');
+ }
+
+ return g_string_free (str, FALSE);
+}
+
+
+static void
+guile_shell(void* closure, int argc, char** argv)
+{
+ scm_shell(argc, argv);
+}
+
+gboolean
+mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err)
+{
+ const char* s;
+ char * mainargs, *expr;
+ char** argv;
+
+ g_return_val_if_fail(msi, FALSE);
+ g_return_val_if_fail(muhome, FALSE);
+
+ if (access(mu_script_info_path(msi), R_OK) != 0) {
+ mu_util_g_set_error(err,
+ MU_ERROR_FILE_CANNOT_READ,
+ "failed to read script: %s",
+ g_strerror(errno));
+ return FALSE;
+ }
+
+ argv = g_new0(char*, 6);
+ argv[0] = g_strdup(GUILE_BINARY);
+ argv[1] = g_strdup("-l");
+
+ s = mu_script_info_path(msi);
+ argv[2] = g_strdup(s ? s : "");
+
+ mainargs = quoted_from_strv(args);
+ expr = g_strdup_printf("(main '(\"%s\" \"--muhome=%s\" %s))",
+ mu_script_info_name(msi),
+ muhome,
+ mainargs ? mainargs : "");
+
+ g_free(mainargs);
+ argv[3] = g_strdup("-c");
+ argv[4] = expr;
+
+ scm_boot_guile(5, argv, guile_shell, NULL);
+
+ /* never reached but let's be correct(TM)*/
+ g_strfreev(argv);
+ return TRUE;
+}
+#else /*!BUILD_GUILE*/
+gboolean
+mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err)
+{
+ mu_util_g_set_error(err, MU_ERROR_INTERNAL, "this mu does not have guile support");
+ return FALSE;
+}
+#endif /*!BUILD_GUILE*/
--- /dev/null
+/*
+** Copyright (C) 2012-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_SCRIPT_HH__
+#define MU_SCRIPT_HH__
+
+#include <glib.h>
+
+/* Opaque structure with information about a script */
+struct MuScriptInfo;
+
+/**
+ * get the name of the script (sans-extension, if some extension was
+ * provided to mu_script_get_scripts)
+ *
+ * @param msi a MuScriptInfo structure
+ *
+ * @return the name
+ */
+const char* mu_script_info_name(MuScriptInfo* msi);
+
+/**
+ * get the full filesystem path of the script
+ *
+ * @param msi a MuScriptInfo structure
+ *
+ * @return the path
+ */
+const char* mu_script_info_path(MuScriptInfo* msi);
+
+/**
+ * get a one-line description for the script
+ *
+ * @param msi a MuScriptInfo structure
+ *
+ * @return the description, or NULL if there was none
+ */
+const char* mu_script_info_one_line(MuScriptInfo* msi);
+
+/**
+ * get a full description for the script
+ *
+ * @param msi a MuScriptInfo structure
+ *
+ * @return the description, or NULL if there was none
+ */
+const char* mu_script_info_description(MuScriptInfo* msi);
+
+/**
+ * check whether either the name or one-line description of a
+ * MuScriptInfo matches regular expression rxstr
+ *
+ * @param msi a MuScriptInfo
+ * @param rxstr a regular expression string
+ * @param err receives error information
+ *
+ * @return TRUE if it matches, FALSE if not or in case of error
+ */
+gboolean mu_script_info_matches_regex(MuScriptInfo* msi, const char* rxstr, GError** err);
+
+/**
+ * Get the list of all scripts in path with extension ext
+ *
+ * @param path a file system path
+ * @param ext an extension (e.g., ".scm"), or NULL
+ * @param prefix for the one-line description
+ * (e.g., ";; DESCRIPTION: "), or NULL
+ * @param err receives error information, if any
+ *
+ * @return a list of Mu
+ */
+GSList* mu_script_get_script_info_list(const char* path,
+ const char* ext,
+ const char* descprefix,
+ GError** err);
+
+/**
+ * destroy a list of MuScriptInfo* objects
+ *
+ * @param scriptslst a list of MuScriptInfo* objects
+ */
+void mu_script_info_list_destroy(GSList* lst);
+
+/**
+ * find the MuScriptInfo object for the first script with a certain
+ * name, or return NULL if not found.
+ *
+ * @param lst a list of MuScriptInfo* objects
+ * @param name the name to search for
+ *
+ * @return a MuScriptInfo* object, or NULL if not found.
+ */
+MuScriptInfo* mu_script_find_script_with_name(GSList* lst, const char* name);
+
+/**
+ * run the guile script at path
+ *
+ * @param msi MuScriptInfo object for the script
+ * @param muhome path to the mu home dir
+ * @param args NULL-terminated array of strings (argv for the script)
+ * @param err receives error information
+ *
+ * @return FALSE in case of error -- otherwise, this function will
+ * _not return_
+ */
+gboolean
+mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err);
+
+#endif /*MU_SCRIPT_HH__*/
--- /dev/null
+/*
+** 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 "mu-runtime.hh"
+#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-parser.hh"
+#include "utils/mu-readline.hh"
+
+using namespace Mu;
+using namespace Command;
+
+/// @brief object to manage the server-context for all commands.
+struct Server::Private {
+ Private(Store& store, Output output)
+ : store_{store}, output_{output}, command_map_{make_command_map()},
+ keep_going_{true}
+ {}
+
+ ~Private() {
+ indexer().stop();
+ if (index_thread_.joinable())
+ index_thread_.join();
+ }
+ //
+ // construction helpers
+ //
+ CommandMap make_command_map();
+
+ //
+ // acccessors
+ Store& store() { return store_; }
+ const Store& store() const { return store_; }
+ Indexer& indexer() { return store().indexer(); }
+ const CommandMap& command_map() const { return command_map_; }
+
+ //
+ // invoke
+ //
+ bool invoke(const std::string& expr) noexcept;
+
+ //
+ // output
+ //
+ void output_sexp(Sexp&& sexp,Server::OutputFlags flags = {}) const {
+ if (output_)
+ output_(std::move(sexp), flags);
+ }
+
+ void output_sexp(Sexp::List&& lst, Server::OutputFlags flags = {}) const {
+ output_sexp(Sexp::make_list(std::move(lst)), flags);
+ }
+ size_t output_results(const QueryResults& qres, size_t batch_size) const;
+
+ //
+ // handlers for various commands.
+ //
+ void add_handler(const Parameters& params);
+ void compose_handler(const Parameters& params);
+ void contacts_handler(const Parameters& params);
+ void find_handler(const Parameters& params);
+ void help_handler(const Parameters& params);
+ void index_handler(const Parameters& params);
+ void move_handler(const Parameters& params);
+ void mkdir_handler(const Parameters& params);
+ void ping_handler(const Parameters& params);
+ void quit_handler(const Parameters& params);
+ void remove_handler(const Parameters& params);
+ void sent_handler(const Parameters& params);
+ void view_handler(const Parameters& params);
+
+private:
+ // helpers
+ Sexp build_message_sexp(const Message& msg,
+ Store::Id docid,
+ const Option<QueryMatch&> qm) const;
+
+ Sexp::List move_docid(Store::Id docid, Option<std::string> flagstr,
+ bool new_name, bool no_view);
+
+ Sexp::List perform_move(Store::Id docid,
+ const Message& msg,
+ const std::string& maildirarg,
+ Flags flags,
+ bool new_name,
+ bool no_view);
+
+ bool maybe_mark_as_read(Store::Id docid, Flags old_flags, bool rename);
+ bool maybe_mark_msgid_as_read(const std::string& msgid, bool rename);
+
+ Store& store_;
+ Server::Output output_;
+ const CommandMap command_map_;
+ std::atomic<bool> keep_going_{};
+ std::thread index_thread_;
+};
+
+static Sexp
+build_metadata(const QueryMatch& qmatch)
+{
+ Sexp::List mdata;
+
+ auto symbol_t = [] { return Sexp::make_symbol("t"); };
+
+ mdata.add_prop(":path", Sexp::make_string(qmatch.thread_path));
+ mdata.add_prop(":level", Sexp::make_number(qmatch.thread_level));
+ mdata.add_prop(":date", Sexp::make_string(qmatch.thread_date));
+
+ Sexp::List dlist;
+ const auto td{::atoi(qmatch.thread_date.c_str())};
+ dlist.add(Sexp::make_number((unsigned)(td >> 16)));
+ dlist.add(Sexp::make_number((unsigned)(td & 0xffff)));
+ dlist.add(Sexp::make_number(0));
+ mdata.add_prop(":date-tstamp", Sexp::make_list(std::move(dlist)));
+
+ if (qmatch.has_flag(QueryMatch::Flags::Root))
+ mdata.add_prop(":root", symbol_t());
+ if (qmatch.has_flag(QueryMatch::Flags::Related))
+ mdata.add_prop(":related", symbol_t());
+ if (qmatch.has_flag(QueryMatch::Flags::First))
+ mdata.add_prop(":first-child", symbol_t());
+ if (qmatch.has_flag(QueryMatch::Flags::Last))
+ mdata.add_prop(":last-child", symbol_t());
+ if (qmatch.has_flag(QueryMatch::Flags::Orphan))
+ mdata.add_prop(":orphan", symbol_t());
+ if (qmatch.has_flag(QueryMatch::Flags::Duplicate))
+ mdata.add_prop(":duplicate", symbol_t());
+ if (qmatch.has_flag(QueryMatch::Flags::HasChild))
+ mdata.add_prop(":has-child", symbol_t());
+ if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject))
+ mdata.add_prop(":thread-subject", symbol_t());
+
+ return Sexp::make_list(std::move(mdata));
+}
+
+/*
+ * A message here is a Sexp::List consists of a message s-expression with
+ * optionally a :meta expression added.
+ */
+Sexp
+Server::Private::build_message_sexp(const Message& msg,
+ Store::Id docid,
+ const Option<QueryMatch&> qm) const
+{
+ auto sexp_list = msg.to_sexp_list();
+ if (docid != 0)
+ sexp_list.add_prop(":docid", Sexp::make_number(docid));
+ if (qm)
+ sexp_list.add_prop(":meta", build_metadata(*qm));
+
+ return Sexp::make_list(std::move(sexp_list));
+}
+
+CommandMap
+Server::Private::make_command_map()
+{
+ CommandMap cmap;
+
+ 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(
+ "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(
+ "mkdir",
+ CommandInfo{
+ ArgMap{{":path", ArgInfo{Type::String, true, "location for the new maildir"}}},
+ "create a new maildir",
+ [&](const auto& params) { mkdir_handler(params); }});
+ cmap.emplace(
+ "ping",
+ CommandInfo{
+ ArgMap{
+ {":queries",
+ ArgInfo{Type::List, false, "queries for which to get read/unread numbers"}},
+ {":skip-dups",
+ ArgInfo{Type::Symbol,
+ false,
+ "whether to exclude messages with duplicate message-ids"}},
+ },
+ "ping the mu-server and get information in response",
+ [&](const auto& params) { ping_handler(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 errcode, const char* frm, ...)
+{
+ char* msg{};
+ va_list ap;
+
+ va_start(ap, frm);
+ g_vasprintf(&msg, frm, ap);
+ va_end(ap);
+
+ Sexp::List err;
+ err.add_prop(":error", Sexp::make_number(static_cast<int>(errcode)));
+ err.add_prop(":message", Sexp::make_string(msg));
+ g_free(msg);
+
+ return Sexp::make_list(std::move(err));
+}
+
+bool
+Server::Private::invoke(const std::string& expr) noexcept
+{
+ if (!keep_going_)
+ return false;
+
+ try {
+ auto call{Sexp::Sexp::make_parse(expr)};
+ Command::invoke(command_map(), call);
+
+ } 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 Parameters& params)
+{
+ auto path{get_string_or(params, ":path")};
+ const auto docid_res{store().add_message(path)};
+
+ if (!docid_res)
+ throw docid_res.error();
+
+ const auto docid{docid_res.value()};
+
+ Sexp::List expr;
+ expr.add_prop(":info", Sexp::make_symbol("add"));
+ expr.add_prop(":path", Sexp::make_string(path));
+ expr.add_prop(":docid", Sexp::make_number(docid));
+
+ output_sexp(Sexp::make_list(std::move(expr)));
+
+ auto msg_res{store().find_message(docid)};
+ if (!msg_res)
+ throw Error(Error::Code::Store,
+ "failed to get message at %s (docid=%u)",
+ path.c_str(), docid);
+
+ Sexp::List update;
+ update.add_prop(":update", build_message_sexp(msg_res.value(), docid, {}));
+ output_sexp(Sexp::make_list(std::move(update)));
+}
+
+/* '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::List pi;
+
+ if (auto cdescr = part.content_description(); cdescr)
+ pi.add_prop(":description", Sexp::make_string(*cdescr));
+ else if (cooked_name)
+ pi.add_prop(":description", Sexp::make_string(cooked_name.value()));
+
+ pi.add_prop(":file-name", Sexp::make_string(fname));
+ pi.add_prop(":mime-type", Sexp::make_string(
+ part.mime_type().value_or("application/octet-stream")));
+
+ return Some(Sexp::make_list(std::move(pi)));
+}
+
+
+void
+Server::Private::compose_handler(const Parameters& params)
+{
+ const auto ctype{get_symbol_or(params, ":type")};
+
+ Sexp::List comp_lst;
+ comp_lst.add_prop(":compose", Sexp::make_symbol(std::string(ctype)));
+
+
+ if (ctype == "reply" || ctype == "forward" ||
+ ctype == "edit" || ctype == "resend") {
+
+ const unsigned docid{(unsigned)get_int_or(params, ":docid")};
+ auto msg{store().find_message(docid)};
+ if (!msg)
+ throw Error{Error::Code::Store, "failed to get message %u", docid};
+
+ comp_lst.add_prop(":original", build_message_sexp(msg.value(), docid, {}));
+
+ if (ctype == "forward") {
+ // when forwarding, attach any attachment in the orig
+ size_t index{};
+ Sexp::List 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.add_prop(":include",
+ Sexp::make_list(std::move(attseq)));
+ comp_lst.add_prop(":cache-path",
+ Sexp::make_string(*msg->cache_path()));
+ }
+ }
+
+ } else if (ctype != "new")
+ throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'",
+ ctype.c_str());
+
+ output_sexp(std::move(comp_lst));
+}
+
+void
+Server::Private::contacts_handler(const Parameters& params)
+{
+ const auto personal = get_bool_or(params, ":personal");
+ const auto afterstr = get_string_or(params, ":after");
+ const auto tstampstr = get_string_or(params, ":tstamp");
+ const auto maxnum = get_int_or(params, ":maxnum", 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::List 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(Sexp::make_string(ci.display_name(true/*encode-if-needed*/)));
+ return maxnum == 0 || n < maxnum;
+ });
+
+ Sexp::List seq;
+ seq.add_prop(":contacts", Sexp::make_list(std::move(contacts)));
+ seq.add_prop(":tstamp",
+ Sexp::make_string(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(std::move(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 msgid-query");
+ 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 Parameters& params)
+{
+ auto docid{get_int_or(params, ":docid", 0)};
+ const auto msgid{get_string_or(params, ":msgid")};
+
+ if ((docid == 0) == msgid.empty())
+ throw Error(Error::Code::InvalidArgument,
+ "precisely one of docid and msgid must be specified");
+
+ if (docid != 0)
+ return {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::List headers;
+
+ const auto output_batch = [&](Sexp::List&& hdrs) {
+ Sexp::List batch;
+ batch.add_prop(":headers", Sexp::make_list(std::move(hdrs)));
+ output_sexp(std::move(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)};
+ msgsexp.formatting_opts |= Sexp::FormattingOptions::SplitList;
+ 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 Parameters& params)
+{
+ const auto q{get_string_or(params, ":query")};
+ const auto threads{get_bool_or(params, ":threads", false)};
+ // perhaps let mu4e set this as frame-lines of the appropriate frame.
+ const auto batch_size{get_int_or(params, ":batch-size", 110)};
+ const auto sortfieldstr{get_symbol_or(params, ":sortfield", "")};
+ const auto descending{get_bool_or(params, ":descending", false)};
+ const auto maxnum{get_int_or(params, ":maxnum", -1 /*unlimited*/)};
+ const auto skip_dups{get_bool_or(params, ":skip-dups", false)};
+ const auto include_related{get_bool_or(params, ":include-related", false)};
+
+ auto sort_field = std::invoke([&]()->Option<Field>{
+ if (sortfieldstr.size() < 2)
+ return Nothing;
+ else
+ return field_from_name(sortfieldstr.substr(1));
+ });
+ if (!sort_field && !sortfieldstr.empty())
+ throw Error{Error::Code::InvalidArgument, "invalid sort field '%s'",
+ sortfieldstr.c_str()};
+ 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;
+
+ 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");
+
+ /* 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. */
+ {
+ Sexp::List lst;
+ lst.add_prop(":erase", Sexp::make_symbol("t"));
+ output_sexp(std::move(lst));
+ }
+
+ const auto foundnum{output_results(*qres, static_cast<size_t>(batch_size))};
+
+ {
+ Sexp::List lst;
+ lst.add_prop(":found", Sexp::make_number(foundnum));
+ output_sexp(std::move(lst));
+ }
+}
+
+void
+Server::Private::help_handler(const Parameters& params)
+{
+ const auto command{get_symbol_or(params, ":command", "")};
+ const auto full{get_bool_or(params, ":full", !command.empty())};
+
+ if (command.empty()) {
+ std::cout << ";; Commands are s-expressions of the form\n"
+ << ";; (<command-name> :param1 val1 :param2 val2 ...)\n"
+ << ";; For instance:\n;; (help :command quit)\n"
+ << ";; to get detailed information about the 'quit'\n;;\n";
+ std::cout << ";; The following commands are available:\n\n";
+ }
+
+ std::vector<std::string> names;
+ for (auto&& name_cmd : command_map())
+ names.emplace_back(name_cmd.first);
+ std::sort(names.begin(), names.end());
+
+ for (auto&& name : names) {
+ const auto& info{command_map().find(name)->second};
+
+ if (!command.empty() && name != command)
+ continue;
+
+ if (!command.empty())
+ std::cout << ";; "
+ << format("%-10s -- %s\n", name.c_str(), info.docstring.c_str());
+ else
+ std::cout << ";; " << name.c_str() << " -- " << info.docstring.c_str()
+ << '\n';
+ if (!full)
+ continue;
+
+ for (auto&& argname : info.sorted_argnames()) {
+ const auto& arg{info.args.find(argname)};
+ std::cout << ";; "
+ << format("%-17s : %-24s ",
+ arg->first.c_str(),
+ to_string(arg->second).c_str());
+ std::cout << " " << arg->second.docstring << "\n";
+ }
+ std::cout << ";;\n";
+ }
+}
+
+static Sexp::List
+get_stats(const Indexer::Progress& stats, const std::string& state)
+{
+ Sexp::List lst;
+
+ lst.add_prop(":info", Sexp::make_symbol("index"));
+ lst.add_prop(":status", Sexp::make_symbol(std::string{state}));
+ lst.add_prop(":checked", Sexp::make_number(stats.checked));
+ lst.add_prop(":updated", Sexp::make_number(stats.updated));
+ lst.add_prop(":cleaned-up", Sexp::make_number(stats.removed));
+
+ return lst;
+}
+
+void
+Server::Private::index_handler(const Parameters& params)
+{
+ Mu::Indexer::Config conf{};
+ conf.cleanup = get_bool_or(params, ":cleanup");
+ conf.lazy_check = get_bool_or(params, ":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)] {
+ 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 Parameters& params)
+{
+ const auto path{get_string_or(params, ":path")};
+ if (auto&& res = maildir_mkdir(path, 0755, FALSE); !res)
+ throw res.error();
+
+ Sexp::List lst;
+ lst.add_prop(":info", Sexp::make_string("mkdir"));
+ lst.add_prop(":message", Sexp::make_string(format("%s has been created", path.c_str())));
+
+ output_sexp(std::move(lst));
+}
+
+Sexp::List
+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();
+
+ const auto new_msg = store().move_message(docid, maildir, flags, new_name);
+ if (!new_msg)
+ throw new_msg.error();
+
+ Sexp::List seq;
+ seq.add_prop(":update", build_message_sexp(new_msg.value(), docid, {}));
+ /* note, the :move t thing is a hint to the frontend that it
+ * could remove the particular header */
+ if (different_mdir)
+ seq.add_prop(":move", Sexp::make_symbol("t"));
+ if (!no_view)
+ seq.add_prop(":maybe-view", Sexp::make_symbol("t"));
+
+ return seq;
+}
+
+
+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();
+}
+
+Sexp::List
+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);
+ auto lst = perform_move(docid, *msg, "", flags, new_name, no_view);
+ return lst;
+}
+
+/*
+ * 'move' moves a message to a different maildir and/or changes its
+ * flags. parameters are *either* a 'docid:' or 'msgid:' pointing to
+ * the message, a 'maildir:' for the target maildir, and a 'flags:'
+ * parameter for the new flags.
+ *
+ * returns an (:update <new-msg-sexp>)
+ *
+ */
+void
+Server::Private::move_handler(const Parameters& params)
+{
+ auto maildir{get_string_or(params, ":maildir")};
+ const auto flagopt{get_string(params, ":flags")};
+ const auto rename{get_bool_or(params, ":rename")};
+ const auto no_view{get_bool_or(params, ":noupdate")};
+ const auto docids{determine_docids(store_, params)};
+
+ if (docids.size() > 1) {
+ if (!maildir.empty()) // ie. duplicate message-ids.
+ throw Mu::Error{Error::Code::Store,
+ "can't move multiple messages at the same time"};
+ // multi.
+ for (auto&& docid : docids)
+ output_sexp(move_docid(docid, flagopt,
+ rename, no_view));
+ return;
+ }
+ auto docid{docids.at(0)};
+ auto msg = store().find_message(docid)
+ .or_else([]{throw Error{Error::Code::InvalidArgument,
+ "could not create message"};}).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);
+ output_sexp(perform_move(docid, msg, maildir, flags, rename, no_view));
+}
+
+void
+Server::Private::ping_handler(const Parameters& params)
+{
+ const auto storecount{store().size()};
+ if (storecount == (unsigned)-1)
+ throw Error{Error::Code::Store, "failed to read store"};
+
+ const auto queries{get_string_vec(params, ":queries")};
+ Sexp::List 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)};
+
+ Sexp::List lst;
+ lst.add_prop(":query", Sexp::make_string(q));
+ lst.add_prop(":count", Sexp::make_number(count));
+ lst.add_prop(":unread", Sexp::make_number(unread));
+
+ qresults.add(Sexp::make_list(std::move(lst)));
+ }
+
+ Sexp::List addrs;
+ for (auto&& addr : store().properties().personal_addresses)
+ addrs.add(Sexp::make_string(addr));
+
+ Sexp::List lst;
+ lst.add_prop(":pong", Sexp::make_string("mu"));
+
+ Sexp::List proplst;
+ proplst.add_prop(":version", Sexp::make_string(VERSION));
+ proplst.add_prop(":personal-addresses", Sexp::make_list(std::move(addrs)));
+ proplst.add_prop(":database-path", Sexp::make_string(store().properties().database_path));
+ proplst.add_prop(":root-maildir", Sexp::make_string(store().properties().root_maildir));
+ proplst.add_prop(":doccount", Sexp::make_number(storecount));
+ proplst.add_prop(":queries", Sexp::make_list(std::move(qresults)));
+
+ lst.add_prop(":props", Sexp::make_list(std::move(proplst)));
+
+ output_sexp(std::move(lst));
+}
+
+void
+Server::Private::quit_handler(const Parameters& params)
+{
+ keep_going_ = false;
+}
+
+void
+Server::Private::remove_handler(const Parameters& params)
+{
+ const auto docid{get_int_or(params, ":docid")};
+ 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);
+ // act as if it worked.
+
+ Sexp::List lst;
+ lst.add_prop(":remove", Sexp::make_number(docid));
+
+ output_sexp(std::move(lst));
+}
+
+void
+Server::Private::sent_handler(const Parameters& params)
+{
+ const auto path{get_string_or(params, ":path")};
+ const auto docid = store().add_message(path);
+ if (!docid)
+ throw Error{Error::Code::Store, "failed to add path"};
+
+ Sexp::List lst;
+ lst.add_prop(":sent", Sexp::make_symbol("t"));
+ lst.add_prop(":path", Sexp::make_string(path));
+ lst.add_prop(":docid", Sexp::make_number(docid.value()));
+
+ output_sexp(std::move(lst));
+}
+
+bool
+Server::Private::maybe_mark_as_read(Store::Id docid, Flags oldflags, bool rename)
+{
+ const auto newflags{flags_from_delta_expr("+S-u-N", oldflags)};
+ if (!newflags || oldflags == *newflags)
+ return false; // nothing to do.
+
+ const auto msg = store().move_message(docid, {}, newflags, rename);
+ if (!msg)
+ throw msg.error();
+
+ /* send an update */
+ Sexp::List update;
+ update.add_prop(":update", build_message_sexp(*msg, docid, {}));
+ output_sexp(Sexp::make_list(std::move(update)));
+
+ g_debug("marked message %d as read => %s", docid, msg->path().c_str());
+
+ return true;
+}
+
+bool
+Server::Private::maybe_mark_msgid_as_read(const std::string& msgid, bool rename) try
+{
+ const auto docids = docids_for_msgid(store_, msgid);
+ if (!docids.empty())
+ g_debug("marking %zu messages with message-id '%s' as read",
+ docids.size(), msgid.c_str());
+
+ for (auto&& docid: docids)
+ if (auto msg{store().find_message(docid)}; msg)
+ maybe_mark_as_read(docid, msg->flags(), rename);
+
+ return true;
+
+} catch (...) { /* not fatal */
+ g_warning("failed to mark <%s> as read", msgid.c_str());
+ return false;
+}
+
+void
+Server::Private::view_handler(const Parameters& params)
+{
+ const auto mark_as_read{get_bool_or(params, ":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(), params)};
+
+ 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 (mark_as_read) {
+ // maybe mark the main message as read.
+ maybe_mark_as_read(docid, msg.flags(), rename);
+ /* maybe mark _all_ messsage with same message-id as read */
+ maybe_mark_msgid_as_read(msg.message_id(), rename);
+ }
+
+ Sexp::List seq;
+ seq.add_prop(":view", build_message_sexp(msg, docid, {}));
+ output_sexp(std::move(seq));
+}
+
+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);
+}
--- /dev/null
+/*
+** 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(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__ */
--- /dev/null
+/*
+** 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-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 = 250'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);
+
+ /* 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)
+{
+ msg.update_cached_sexp();
+
+ 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);
+ 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)
+{
+ msg.update_cached_sexp();
+
+ 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 (properties().schema_version == ExpectedSchemaVersion)
+ return; // all is good.
+
+ // Now, it seems the schema versions do not match; shall we automatically
+ // update?
+
+ if (none_of(opts & Store::Options::AutoUpgrade)) {
+ // not allowed to auto-upgrade, so we give up.
+ throw Mu::Error(Error::Code::SchemaMismatch,
+ "expected schema-version %s, but got %s; "
+ "cannot auto-upgrade; please use 'mu init'",
+ ExpectedSchemaVersion,
+ properties().schema_version.c_str());
+ }
+
+ // Okay, let's attempt an auto-upgrade.
+ g_info("attempt reinit database from schema %s --> %s",
+ properties().schema_version.c_str(), ExpectedSchemaVersion);
+
+ Config conf;
+ conf.batch_size = properties().batch_size;
+ conf.max_message_size = properties().max_message_size;
+
+ priv_.reset();
+ priv_ = std::make_unique<Private>(path,
+ properties().root_maildir,
+ properties().personal_addresses,
+ conf);
+ // Now let's try again.
+ priv_.reset();
+ priv_ = std::make_unique<Private>(path, none_of(opts & Store::Options::Writable));
+ if (properties().schema_version != ExpectedSchemaVersion)
+ // Nope, we failed.
+ throw Mu::Error(Error::Code::SchemaMismatch,
+ "failed to auto-upgrade from %s to %s; "
+ "please use 'mu init'",
+ properties().schema_version.c_str(),
+ ExpectedSchemaVersion);
+}
+
+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);
+}
+
+
+Result<Message>
+Store::move_message(Store::Id id,
+ Option<const std::string&> target_mdir,
+ Option<Flags> new_flags, bool change_name)
+{
+ 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);
+
+ 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, change_name);
+ 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 = priv_->update_message_unlocked(*msg, old_path); !res)
+ return Err(res.error());
+
+ /* 6. Profit! */
+ return Ok(std::move(msg.value()));
+}
+
+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{});
+}
--- /dev/null
+/*
+** 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.
+ *
+ * @param path
+ * @param readonly
+ */
+ enum struct Options {
+ None = 0, /**< No specific options */
+ Writable = 1 << 0, /**< Open in writable mode */
+ AutoUpgrade = 1 << 1, /**< automatically re-initialize
+ * versions do not match */
+ };
+
+ /**
+ * Make a store for an existing document database
+ *
+ * @param path path to the database
+ * @param options startup options
+ *
+ * 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
+ */
+ 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;
+
+ /**
+ * 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 the moved message or some error.
+ */
+ Result<Message> move_message(Store::Id id,
+ Option<const std::string&> target_mdir = Nothing,
+ Option<Flags> new_flags = Nothing,
+ bool change_name = false);
+
+ /**
+ * 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);
+
+} // namespace Mu
+
+#endif /* __MU_STORE_HH__ */
--- /dev/null
+/*
+** 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;
+}
--- /dev/null
+/*
+** 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__ */
--- /dev/null
+/*
+** 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,
+ 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::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__ */
--- /dev/null
+/*
+** 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 auto parts{split(field_val.value(), " ")};
+ if (parts.empty())
+ return Xapian::Query::MatchNothing; // shouldn't happen
+ else if (parts.size() == 1)
+ return make_query(field_val, true /*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:
+ 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
+}
--- /dev/null
+/*
+** 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__ */
--- /dev/null
+/*
+** 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 <regex>
+#include <fstream>
+
+#include <utils/mu-utils.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
+ 5rDHoN1+/ScH/O9/G1fKWbGryQ203thF+1ZrHUOOwq8MVOc5SsoqzSTsaNbEAdthFcDDz3Oui
+ SHxX1hdpV3UOjZEHzWlpjEjRe7t74g2RI/ESELmkPuLg/LZC7SjAsg70cTJBIfDPYxJkJAcUl
+ 9W6OEBsmtTDO0va/EQRYjfkpoF9tjfmfMNw9KSKHuDdqZu2Xfak8mQKnWsoxWeUkD31r60iPC
+ yikbj7KP5AlHaWMzyTTdlvtjYRLfSuUSe1uqjI5NWCnZDDjz7zODoaWPp7p2U/MQenXEjN6+M
+ WnZL6ZC8AGtze/hYgOCXcLf4ydQ7m9YueJiY5nDn7g+cwnhxypVNFTL5NjSpKKXbkzbyu9Tdl
+ ez+92g/9pGW17iOo5NrFtfctLlmCEH0RxjouKI7FBmv3bIvFC4FvfghiNf7OZmRg2/nT5i+1o
+ AICYNAx2y5CezKsKM2f1tm60dkydQIR8pK45dDKZPz3i7NeJm9dknZ2OYFTnucUvdPaT8nR43
+ cK3kk2QUE48Ngo/0NwepSGrV9TkOt+hY3PUYkXWp/mwP2QPSjy4cALyvLyKwG24qZ9CiiRLMV
+ KqPFlCRnoDG5MHJ4d0krFlqmg8rNsWzV3oWfMNKFZmD24lVUmWGb+ExxbCFc0xzIt12o/EqBw
+ nVkXLu+E0apM+cmG6ubYfOymRoUpiKsZI9ivc+mAaEE+v2RBzcAURzlhzQHIn81onvzbQwCge
+ tMtBkSEfqyoa1HjalX4B9WQ90M42K+7xW039ydakQ7JOeYVpkPXYoBF7mbrXRckhMXjQatLQ8
+ MWA8+U031Xfa1ueOIfCCkzJ49wyx1LoLPyqdjCvnzaRd72yNEMJ5zM/itMIPE9reIHBtpom0i
+ RhIYdJFDrL+SKqE68lcJakCcF3R+VLApwLKOr0HChGQjdEk7c/rm5E0dF5f3oYlHf591QoXIJ
+ h16yfcJYe6fMo1YYunkvbEDFPpzttIq7aIk0FzxrOdRvj3yQajDbwOpYI/5T/DaabPn3M8lK7
+ 8pn7LrbmyCaLHhkYMS4h3SDkYWsifza6vkldizrK7IPf6KhS7AhTkbnEonWS6454GLUg1nYGX
+ W5Qp/G9LzvjtEGQMcwnCN5jb5zq7o3f+9FrROKjpwFxL+mL85CEXY/KMOVpf6hDJJfSyu6/X6
+ FpbwlJVLFdGeA0/+xcKcmutpkJACgK2kHqvZ8MZxt+5jBJWVlIDLZKa8/IoGWC+ikLX2/hPNB
+ 4TU89QYG5ygPmwwDXruFG7N7jVURZceHqWNKtqegS6YQ5nirsPJWJR7jzgr+HbntUaQETXNpn
+ QrxpsVHXfqRu2GlP5h28RaIpvBVUcwqrs+eLJELStvBzyAmaVPVoKFjEWFfwrmE89W6Bmz2W3
+ kHExOq3hI3gDsGXKjTjT/kjHkaHmtnVUXr4vqovf8Ht4Vwmtf0S4xsgpYjnYjUIzG9eiwIFAZ
+ hL2gvjwW51qtMvybf01C50xTiS9GSfO0SR7meBPA67skcA+wFo11wmwXsUk1irpKnC+Y9hVZX
+ 2vPkfZ1T2VXNo997cQC59lBpi/TU5gnuM7H/Vcl7tF3Lqtmqut7s6HkPWCegDZ3O2W7shH7aZ
+ 1bOXbO+W/SNC+WcMnj+fhuP+dHcrt0Vw4RD9knJOOzdZTH3OCli/vpjqgTbCKEaWMhCIeM2g0
+ RiLFxTeTEEBCa49bwa8n2r4T/vA3duZd8F/DNKvWTfhRr1Mxtz3n15EOar13fFijtnieEiv4/
+ vO/5uRF+H86Fcoua7B8AswThbiG1vou6M48g0Zo6iGEcrueKEaHMI4XM7wQF77KazMdn5f1BP
+ +KyQX83aHJN/qGniXgF8yu+h0M7Nf0YrTteYQd2C/HZrIA8IaLqqvLoGRl7dRBnbZiP7jRdQm
+ 1YEYtjX4XBoShrXPfIxPnJBUBnnOaePYxOJkS2FaBv19jPkMnyc9xuJYD7JOTFnXKzAnoaBqT
+ OR+dGrLLGZ1MM/0gqclKTv7Hcce+6CJyTWkx5mq42w49HFI/kdHBRxU8xIRv4B9l0ePf9EbWr
+ cDcrssee//6KXiRmF4fm7jq828/uhj8MIJet9sIU5ncKwHEse3I4YmVT5+dB+ZGZh0gbJPFj6
+ xcICpshhYct+euMCdNfy3lkxiRr76RwfBzLAOP5+1U3GAx/hcsL2AgyBHMwWo+Kkeq8pPy4YI
+ pQMxJyylI6JMa/DbBggnDk+xNZpRKo/XA4lAJY57DCOPL2ZcL8kU2aCd5LjtYHK0ZWSFtOjxs
+ oIEr/f2vvg+zibxzaANBzylZn3yPe9pI/IBefu9fL4MVaYY3aboxuncX4fyi0VH0WbFkSYXRi
+ a7LIu3LI2LTU13C/LE7j9hmxP6TApyiXi14f0GSa2sbF6HWp2v2rhYM7h67AAn3SQgvcJLpgb
+ Hz5ABb/OAk6ABVEl+a483zexJ6iT2P0gYc08zmewy8Jf8AD9r846k9pGZuhBaOHREx3bA16Bj
+ uWYh3QzSI6MQoJM3XbBGLVkX36Lfj54T9kk97lLaxfbGPuNoyOV9iTBKxts3m2KD+52iH3EEi
+ glbH6HNIUHyCHdEXsXyGVFwfM9V7OQcVO/g266KIQ74wU16x/Zdsq4p/1PcRXHRnoMxP/pUrj
+ EOLWzFU71qzC/OSkYWRil9HXUyucTFGQ0N08jZNXctI9lElWtgq3iI+Cz2F20rz+LJGhSHSkZ
+ 0G5JgXrtspeJN5yoH6TOE0hblr5sZcAM0wiSP7x/hPBeYHswzTA5/laWMn++9aTPVgpPaJ9/x
+ wyLm55OZr4Jl+StWd3MqLCgiRB3cNGrDX7f8Eqnj4wfCHiGIUHewD4qrfXraZQhIk17W+9JyD
+ osmUiVD9ZRdNCY2eNnu8ZkJ4uzKl44lwLL43sInKBjdAHlnoxrR2FOrYXbnU31ujwxdeUr6Hs
+ xPFy0Git0CpWCWYmaz37KA8GW7PE4ffWzcfCmz6AKBrbHcCreeUnyqnSEDy9ubnz7mcLRnu3W
+ RAWi6diI8gcS9g0+r4z5PtZX9rveXRekHJ4k08VuYVmdiz3gjXmHPlm9IKPEAbygP2EYgjwGE
+ 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"
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0BBwEAAKCCEv4w
+ggWpMIIDkaADAgECAhAV2Tfkh0+gtEu0gskeSMTdMA0GCSqGSIb3DQEBCwUAMFsxEjAQBgoJkiaJ
+k/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQx
+FzAVBgNVBAMTDmFkLUdJTEdBTEFELUNBMB4XDTE2MDcyNDE5NTcxM1oXDTM2MDcyNDIwMDcxMlow
+WzESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYKCZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQBgoJkiaJ
+k/IsZAEZFgJhZDEXMBUGA1UEAxMOYWQtR0lMR0FMQUQtQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQCq+/935KPrc8clxrq76k7GrrUHRbsM4FCfyrWicGPZsOKbJfcoloF2EAfj6AYR
+QyU/l9um/8NqW+cu6/TY6YcY622L+UtT1QWC/Kt0kVL7cTtZN+VK/BkjcDVbUOqdeFY1q0tMzdco
+WFxqjayGRYnX6oEZ7krDsGtJBBET/504Z3vDq/0ZD3lNG2dCWp1y+3VzUcb+OKkOPwMGHpw3gZM5
+lZN/znB7d7qwxFSRoLzZZB3nZKKJHcp2ZuyJR+pCT5VdHGGV4gpVQKuL49/UoJBA0o8Kv0DGPByD
++LVwhlyFMi2jlnCd5lqiWRw9JAE3fqS/Di/cGbMjXMI2CplBj+GmZH8fgy4BQRwmsOUELTaYkJyJ
+otcHGENO1+xYrR/lFEQLhh+8V2IJvBM2G1dgJ3EuEslL4q0xGeYLZJd7Z9xvXkAJaX/eWjHWICFI
+zbsH/6fBqXYow/V8hfZhb20dGGnPESXPqMv/1mLgUIqr++Fjl6zKM5mYZuHlmrtd+eLgg7VsjDvh
+cMxdQnju+jzJflxlmY2KSwt5lsu7viqmQyqVUnHFaEsV116B0uCROc5o1pBdRMdeeLrRoj6xPVlc
+IzmIZz3wZERxCAWeJqBx5d1kXe+cDL4pMNQ/hmah4mshjtyOGv+oEgcdxzUQ72W7JNLhSv8C6gpU
+eQwPq8usFAvUOwIDAQABo2kwZzATBgkrBgEEAYI3FAIEBh4EAEMAQTAOBgNVHQ8BAf8EBAMCAYYw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUF+CLMX/eZk96ElRSeiEHqnsujqEwEAYJKwYBBAGC
+NxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIBACcwALtn+SFUx+YTrLCFY+Ghh4yubQt3YdEI6hOQ
+JnmNPKsUEzCvoRE5L2ZLkG2VhJNX3KAJmXgkZMCGBPbiA/65r/cbYqZATQEG/g9aVicz/IBHXvg4
+7+YDDN9VpRy8c93AZNNTRf83Pw+CDsdIGG7mg8rc0tiCgt0V3gN0wF8oRSsb/trqd+ujk41bvaPw
+Rl+8JUeRN0Pq9lH4VGGk9GEIQv8JXhr2VKFmJcGKLB+qvMRvWQZ5oPTGDE3pUYI5q8f7/fMiJKU6
+hb9l+tXP7uDLWIawg/MoUc2BwAThyXFk9LZhkYWYpzbaf2Ez2JYieD4ey8RjEKvis9mF6Z/p6+69
+GbYvuf2bRikYenrmboXCUO820totjP2UyHczexZsMP/XznmyDJuN+BDLzLjm7ks8lXDwpF/Kqnjm
+1EyiQI0OB4cn889yM039U7raJeHpuiwju2/YO6krE+plLQhkM7pl6v6Ly/ZKICwDfbcU8k8LE4+K
+3VaXmVYRYbSXx8l2Ke0CWKNfehBGQ024gKjNt8t7gCgInG5s+roumqeKyfCWlhYll1FAxEQmwP/6
+966y7uJrGLra0VUjdppbZpAENSF0pdX08VfsasSZ20hnCaLWO1b3i0ZOBLBAoNzeCm+BdS6DAOhy
+JnHHZ+OBoiaYwCSjSvTDmHyQkNK3wmu+/wyNMIIGnDCCBISgAwIBAgITbwAAAEFhCq43is5OqAAA
+AAAAQTANBgkqhkiG9w0BAQsFADBbMRIwEAYKCZImiZPyLGQBGRYCdXMxGDAWBgoJkiaJk/IsZAEZ
+FghudW1lcmljYTESMBAGCgmSJomT8ixkARkWAmFkMRcwFQYDVQQDEw5hZC1HSUxHQUxBRC1DQTAe
+Fw0xOTA3MjIxNDE4MDFaFw0yMTA3MjIxNDI4MDFaMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYG
+CgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNF
+TEVCUklBTi1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKRLgjg0yC0P2jLwTCIA
+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 std::regex& rx, size_t id)
+{
+ char buf[16];
+ ::snprintf(buf, sizeof(buf), "%zu", id);
+ return std::regex_replace(test_msg, rx, buf);
+}
+
+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 = std::regex("@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();
+}
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+## 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'])
--- /dev/null
+/*
+** 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;
+}
--- /dev/null
+/*
+** 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();
+}
--- /dev/null
+/*
+** 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 <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"
+#include "utils/mu-util.h"
+
+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();
+}
--- /dev/null
+/*
+** 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();
+}
--- /dev/null
+/*
+** 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;
+}
--- /dev/null
+/*
+** 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 moved_msg = store.move_message(old_docid, Nothing, Flags::Seen, rename);
+ assert_valid_result(moved_msg);
+ 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 thath the cached sexp for the message has been updated;
+ * that's what mu4e uses */
+ const auto moved_sexp{moved_msg->to_sexp().to_sexp_string()};
+ /* clumsy */
+ g_assert_true(moved_sexp.find(new_path) != std::string::npos);
+
+ /*
+ * 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*/);
+}
+
+
+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);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** 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 <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 "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_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));
+
+ 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->to_sexp().to_sexp_string().c_str());
+
+ // Move the message from new->cur
+ std::this_thread::sleep_for(1s); /* ctime should change */
+ const auto msg3 = store->move_message(msg->docid(), {}, Flags::Seen);
+ assert_valid_result(msg3);
+ 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->to_sexp().to_sexp_string().c_str());
+ g_assert_cmpuint(store->size(), ==, 1);
+}
+
+
+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/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/move", test_index_move);
+ g_test_add_func("/store/index/fail", test_store_fail);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** 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();
+}
--- /dev/null
+/*
+** 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;
+}
--- /dev/null
+/*
+** 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();
+}
--- /dev/null
+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
+
+
--- /dev/null
+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==--
+
--- /dev/null
+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-----
+--=-=-=--
+
+
+
--- /dev/null
+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.
+
--- /dev/null
+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
+
--- /dev/null
+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--
+
--- /dev/null
+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.
--- /dev/null
+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.
--- /dev/null
+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.
+
+
--- /dev/null
+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-----
+--=-=-=--
--- /dev/null
+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
+--=-=-=--
--- /dev/null
+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.
+
+
+
--- /dev/null
+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-----
+--=-=-=--
+
--- /dev/null
+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-----
+--=-=-=--
+
--- /dev/null
+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.
--- /dev/null
+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
+
+
+
+
+
--- /dev/null
+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
+
+
--- /dev/null
+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
+ =================================================
+
--- /dev/null
+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</TITLE>=0A=
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dunicode">=0A=
+<META content=3D"MSHTML 6.00.2715.400" name=3DGENERATOR></HEAD>=0A=
+<BODY>=0A=
+<DIV id=3DidOWAReplyText54900 dir=3Dltr>=0A=
+<DIV dir=3Dltr><FONT face=3DArial color=3D#000000 size=3D2>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.</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT> </DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>I'm pretty sure, if you =
+perform the tests suggested by Mihai, that you will find zero =
+performance difference, neither better, nor worse.</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT> </DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>Paul</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT> </DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>=0A=
+<HR tabIndex=3D-1>=0A=
+</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial><FONT size=3D2><B>From:</B> =
+sqlite-dev-bounces@sqlite.org on behalf of Marco Bambini<BR><B>Sent:</B> =
+Mon 8/4/2008 11:40 AM<BR><B>To:</B> =
+sqlite-dev@sqlite.org<BR><B>Subject:</B> [sqlite-dev] VM optimization =
+inside sqlite3VdbeExec<BR><BR></FONT></FONT></DIV></DIV>=0A=
+<DIV>=0A=
+<P><FONT face=3DArial size=3D2>Inside sqlite3VdbeExec there is a very =
+big switch statement.<BR>In order to increase performance with few =
+modifications to the <BR>original code, why not use this technique =
+?<BR></FONT><A =
+href=3D"http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html">=
+<FONT face=3DArial =
+size=3D2>http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html<=
+/FONT></A><BR><BR><FONT face=3DArial size=3D2>With a properly defined =
+"instructions" array, instead of the switch <BR>statement you can =
+use something like:<BR>goto * =
+instructions[pOp->opcode];<BR>---<BR>Marco Bambini<BR></FONT><A =
+href=3D"http://www.sqlabs.net/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net</FONT></A><BR><A =
+href=3D"http://www.sqlabs.net/blog/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net/blog/</FONT></A><BR><A =
+href=3D"http://www.sqlabs.net/realsqlserver/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net/realsqlserver/</FONT></A><BR><BR><BR><BR><=
+FONT face=3DArial =
+size=3D2>_______________________________________________<BR>sqlite-dev =
+mailing list<BR>sqlite-dev@sqlite.org<BR></FONT><A =
+href=3D"http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev"><FONT=
+ face=3DArial =
+size=3D2>http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</FONT=
+></A><BR></P></DIV></BODY></HTML>
+------_=_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==--
+
--- /dev/null
+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
+ =================================================
+
--- /dev/null
+Return-Path: <>
+X-Original-To: f00f@localhost
+Delivered-To: f00f@localhost
+Received: from puppet (puppet [127.0.0.1])
+ by f00fmachines.nl (Postfix) with ESMTP id A534D39C7F1
+ for <f00f@localhost>; 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 <f00f@localhost> (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: <fwdgrp_11163824_f00f@f00fmachines.nl>
+X-Default-Received-SPF: pass (skip=forwardok (res=PASS)) x-ip-name=192.168.10.123;
+From: ArtOlive <artolive@mailinglijst.nl>
+To: "f00f@f00fmachines.nl" <f00f@f00fmachines.nl>
+Reply-To: <artolive@mailinglijst.nl>
+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
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://ww=
+w.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns=3D"http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3D=
+ISO-8859-15">
+ <title>Artolive</title>
+ </head>
+ <body style=3D"line-height: 13px; font-family: Verdana; color: rgb(11=
+9, 119, 119); font-size: 10px;" vlink=3D"#666666" alink=3D"#666666" link=3D=
+"#666666">
+ <style type=3D"text/css">
+ A {
+ COLOR: #666666; TEXT-DECORATION: none
+ }
+ TD {
+ FONT-FAMILY: Verdana; COLOR: #777777; FONT-SIZE: 10px; VERTIC=
+AL-ALIGN: top
+ }
+ </style>
+ <table style=3D"width: 631px;" width=3D"631" align=3D"center" cel=
+lpadding=3D"0" cellspacing=3D"10">
+ <tbody>
+ <tr>
+ <td><a href=3D"http://www.mailinglijst.eu/redirect.as=
+px?l=3D154041&a=3D10374608&t=3DH" target=3D"_blank"><img style=3D"height:=
+ 42px; width: 188px;" alt=3D"artolive" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/logo.jpg" width=3D"188" border=3D"0" height=3D"42"></a=
+> </td>
+ </tr>
+ <tr>
+ <td style=3D"text-align: right; padding-right: 5px; c=
+olor: rgb(0, 0, 0); font-size: 10px;">ART-O-NEWS • juni 2011 </=
+td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172); padding: 5px 10px 5px 15px; color: rgb(=
+167, 169, 172); font-size: 9px;">Westergasfabriekterrein Polonceau=
+kade 17 1014 DA Amsterdam tel: 020-6758504 <a style=3D"color=
+: rgb(167, 169, 172);" href=3D"mailto:info@artolive.nl">info@artolive.nl<=
+/a> <a style=3D"color: rgb(167, 169, 172);" href=3D"http://www.mail=
+inglijst.eu/redirect.aspx?l=3D154041&a=3D10374608&t=3DH" target=3D"_blank=
+">www.artolive.nl</a> </td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(81, 81, 81);">
+ <table width=3D"100%" cellpadding=3D"10" cellspacing=3D=
+"0">
+ <tbody style=3D"color: rgb(119, 119, 119); font-s=
+ize: 10px; vertical-align: top;">
+ <tr>
+ <td style=3D"vertical-align: top;"><img s=
+tyle=3D"height: 338px; width: 252px;" src=3D"http://mailinglijst.eu/klant=
+en/11909/juni2011/IMG_1698_%28Medium%29.JPG" width=3D"252" height=3D"338"=
+> </td>
+ <td style=3D"vertical-align: top;"><span =
+style=3D"line-height: normal; font-size: 24px; color: rgb(0, 0, 0);">Juni=
+ expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers</span><b=
+r>
+ <p><strong>Zondag 5 juni</strong><br>
+ 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. </p>
+ <p>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.</p>
+ <p>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! </p>
+ <br>
+ <p align=3D"right"><img style=3D"height: =
+4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/11909/Sj=
+abloon/green_point.jpg" width=3D"4" height=3D"4"> <strong></strong>=
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154041&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154041&a=3D10374608&t=3DH"><strong>bekijk=
+ meer werk op www.artolive.nl...</strong></a> </p>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+ <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+ <tbody>
+ <tr>
+ <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_uitgelicht_fade.jpg" width=3D"629" height=3D"24"> </td>=
+
+ </tr>
+ <tr>
+ <td>
+ <table width=3D"100%" cellpadding=3D"10" =
+cellspacing=3D"0">
+ <tbody style=3D"color: rgb(119, 119, =
+119); font-size: 10px; vertical-align: top;">
+ <tr>
+ <td style=3D"width: 150px;"><=
+a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154043&a=3D10374608&t=3DH"><img style=3D"height: 214px; width: 156px; bor=
+der-width: 0px; border-style: solid;" src=3D"http://mailinglijst.eu/klant=
+en/11909/juni2011/akker-adam-eva.jpg" width=3D"156" height=3D"214"></a> <=
+/td>
+ <td><span style=3D"color: rgb=
+(0, 0, 0);">Peter van den Akker</span><br>
+ <p>"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.”</p>
+ <p>Peter van den Akker expose=
+ert regelmatig in binnen- en buitenland bij galerieën en musea en is=
+ in verschillende kunstinstellingen en bedrijfscollecties opgenomen.</p>
+ <br>
+ <p align=3D"right"><img style=
+=3D"height: 4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/green_point.jpg" width=3D"4" height=3D"4"> =
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154044&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154043&a=3D10374608&t=3DH"></a><a target=3D=
+"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154044&a=3D=
+10374608&t=3DH"><strong>lees meer over Peter...</strong></a><strong></str=
+ong> </p>
+ </td>
+ </tr>
+ <tr>
+ <td align=3D"center"><a targe=
+t=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154045&=
+a=3D10374608&t=3DH"><img style=3D"border-width: 0px; border-style: solid;=
+ height: 279px; width: 44px;" src=3D"http://mailinglijst.eu/klanten/11909=
+/juni2011/vieleer-thesky032_bijgesneden.jpg" width=3D"44" height=3D"279">=
+</a> </td>
+ <td><span style=3D"color: rgb=
+(0, 0, 0);">Marinel Vieleers</span><br>
+ <p>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.</p>
+ <p>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.</p>
+ <p>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.</p>
+ <br>
+ <p align=3D"right"><img style=
+=3D"height: 4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/green_point.jpg" width=3D"4" height=3D"4"> =
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154045&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154046&a=3D10374608&t=3DH"></a><a target=3D=
+"_blank" href=3D"http://www.artolive.nl/work/165738"><strong>lees meer ov=
+er Marinel...</strong></a> </p>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+ <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+ <tbody>
+ <tr>
+ <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_selection_fade.jpg" width=3D"629" height=3D"24"> </td>
+ </tr>
+ <tr>
+ <td style=3D"padding: 5px 5px 2px;">
+ <table width=3D"100%" cellpadding=3D"5" c=
+ellspacing=3D"0">
+ <tbody>
+ <tr>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154048&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17372=
+0.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154049&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17386=
+9.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154050&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17398=
+0.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154051&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17390=
+5.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154052&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17390=
+4.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154053&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17398=
+4.jpg" width=3D"92" height=3D"92"></a> </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+ <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+ <tbody>
+ <tr>
+ <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_agenda_fade.jpg" width=3D"629" height=3D"24"> </td>
+ </tr>
+ <tr>
+ <td>
+ <table width=3D"100%" cellpadding=3D"10" =
+cellspacing=3D"0">
+ <tbody style=3D"color: rgb(119, 119, =
+119); font-size: 10px; vertical-align: top;" valign=3D"top">
+ <tr>
+ <td valign=3D"top"><br>
+ </td>
+ <td><span style=3D"color: rgb=
+(0, 0, 0);">ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met =
+Marinel Vieleers en Peter van den Akker</span><br>
+ </td>
+ </tr>
+ <tr>
+ <td valign=3D"top"><br>
+ </td>
+ <td>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!<br>
+ </td>
+ </tr>
+ <tr>
+ <td valign=3D"top"><br>
+ </td>
+ <td><span style=3D"color: rgb=
+(0, 0, 0);"></span>Daarna is de expositie te zien op werkdagen (ma - vrij=
+) tussen 10:00 en 17:00. De expositie duurt tot 24 juni 2011.<br>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style=3D"padding: 30px 15px 15px; text-transform:=
+ uppercase; color: rgb(119, 119, 119); font-size: 8px;"><img style=3D"hei=
+ght: 58px; width: 59px;" alt=3D"Kunst Koop" src=3D"http://mailinglijst.eu=
+/klanten/11909/Sjabloon/kunstkoop.jpg" width=3D"59" align=3D"right" heigh=
+t=3D"58"> wil je niet langer door artolive geïnformeerd worden? Klik=
+ dan <a href=3D'http://www.mailinglijst.eu/nieuwsbrief/edit/?e=3Df00f@djc=
+bmachines.nl&c=3D9856&l=3D100549'>hier</a> om je af te melden. <br>
+ kreeg je dit mailtje doorgestuurd en wil je voortaan =
+zelf ook graag de nieuwsbrief ontvangen? <br>
+ klik dan <a href=3D"http://www.mailinglijst.eu/r=
+edirect.aspx?l=3D154054&a=3D10374608&t=3DH" target=3D"_blank">hier</a> om=
+ je aan te melden. </td>
+ </tr>
+ </tbody>
+ </table>
+ <!-- MailingLijst_code --><img src=3D"http://www.mailinglijst.eu/imag=
+es/10374608.109906.aspx" border=3D0><!-- einde MailingLijst_code --><p><C=
+ENTER><SPAN STYLE=3D"COLOR:#d3d3d3;FONT-FAMILY:verdana;FONT-SIZE: 10px"><=
+HR SIZE=3D1 STYLE=3D"COLOR:#d3d3d3" SIZE=3D1>Deze e-mailing is verzorgd m=
+et <a href=3D"http://www.mailinglijst.com" target=3D_blank class=3D"ml_li=
+nk">MailingLijst</a></SPAN></CENTER></p></BODY>
+</html>
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d--
--- /dev/null
+From: Sender <test@example.com>
+To: Recip <recip@example.com>
+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
--- /dev/null
+From: Sitting Bull <sb@example.com>
+To: George Custer <gac@example.com>
+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
+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
+2A7BR0UDoKuJaud54gcYX0Jk/wC46Lbno5T4sf8ALJ/Sqlq+sx3j/wDiOoahq7A5HmSFIwfYf2qA
+uLxpn55ZGkf1Y5ptLPynAxVxEsdQXcwWNrCvuvM33phM8jS+Z5rZBz1pn+JxuWxRGvxzbDI9aYbE
+rHfXgGFuX/T+1OrPXdZs3LWt/NCx6lTjP2xUGl3EO9HN3Gds/ah8XnTvEjiiywZLsXKj92UBs/ff
+9at+heL9jNiPV7FoG7vCdvsf71izzAocGkWkyD1phuPVuh63o+uRc+mX8NwQMlAcOPmp3p8UA7V5
+Gs725sp0mtLiSGRDlWRiCD7EVq3Afi5KrR2HFA8yPoLxB8S/7x+8Pcb/ADqYNfkA3pLlpWKWC6t4
+7m1lSaGReZJEbKsD3BoKtYWEwvSjhQe1GA9aOoFA2K4JoAb9Kdcq5zQEagnYVKpALkHrQpyAB0Az
+6UKioTlIpxCMikyMiloU2qwLRfMU6iGe4puin0p3Ah6kYHf2rUQsgVFLuyqoGSScACsj8TfFURPJ
+pXDEo2+GW8Hf2T29/t61D+MHiM+pTy6BoMpFhGeWedT/AOeR2B/h/n8qyiSQjuc961ImndzdzTyN
+NPK0sjHJLHNNZLk5601klY96SZqrOnRuipJApKWdn70iuTTqztWnkUAEg+gqp9JRrJIQEVmPsM0s
+LG9dsC2lyenwmtO4D4PZ3jmKeYp64GQPnWoDw8WeBZIrZUbqMj+dTVx5l/yvUACfw0gx1yKQaOaM
+kNGy/MV6UuuALhIyGRDj2qj8W8E3do3mCDIJ3wOgppjICzD2owmPepnWdKaAtlSCO2KgGBBwe1VM
+OBJnYUA2BTdTR1zQaD4UceXHDd+thfyNJpE74dTv5JP76+3qO/zr0PHiQB42DKwyrA5BHrXjtc16
+l8Lo75OBNIF+GE34cYDdeTJ5P/bisdRqLGq980cLjpXQpB60dQc1lonymuBd8UsFocveoogXahSm
+2+1CsqglI6E05twu1NU69KdW/XFaiHcYXOKznx44xOi6QvD2ny8t7fITOyneOE7Y+bbj5A+orRlK
+Rq0kjBUQFmJ6ADqa8l8ba5LxDxTf6vITi4lJjH8KDZR9ABW+WUY03KMCkC5cnJorHJoA4rQ6elBI
+2kOFGTjNGUcx32qZsdAubiESoC+eyjcUQwgspVTmZod+ilxk1a+EtC8/UIeZ+WOVSSmNw3/WKjG0
+60tpbeCZJTLIQVKHIO/Tb5Vb7Vbhbxr+1jES82Vjkckuo/d6bHHr7VBtXhrp8ttaxhreGKFfzcw/
+X1rTbdomQBQCpGxA61ROFW/EaBDcsQrcgyo3GcdKnLNTHmWV0VTjdTjb5f8A+UaieFjHK/mHBQZJ
+J6Yqr8XXPD8UDie9tkC5BzKPsKrfGnFGu6jM2k8OlxynlaY/EgPYbd/sKyjTOFeK+JeJmtdUSRgk
+gDM/wDrjI9fuaiadcY22k3EL3MbRgcpCuB136VjOoIou2ZNlJyK9NeKfhUR4fwS6aZDLYEtcIDu4
+I3b6YrzJdczTNzdtvtViEQoNKwxs7qiKWZjgADJJqS4b4e1fiC/Wy0ewmu5m68i/Co9WPQD3NejP
+C7wtseEuTU9UaK+1gj4SBmO3/wBuerf6vtVXFO8K/CR08nXOKoimCHgsGG59DJ//AD9/Sti5eu1O
+5zzHem/b3rPqyCgHNGUUXau5qK6a4a4DXcZqApOxwDQoxWhWK0gsdDinEGAelJMgxS0CgVqMmHHM
+7wcDa3LHs4sJsEdvgIryU29ez0tobmNre4iSWGVSkiMMhlIwQazXjHwGtLsyXfCt8LWQ7i0uCSny
+V+o+ufnXSMvPGN66BVl4m4H4p4clZdW0W7hQHaUJzxn5OuR+tV4Ieb8pG/pVE5w/odxcSiaWNhGm
+CB6g9/lWn6XprWmnk5jifPQ9MHPX5AVQtK1trTTrlDkyOnlg+m2/9BTF77W7iN3Ms5UAFiW6elRF
+6gsNQZ31aOS3gnjCrIzDnJU9cdh9KXKZto52hYWkcgAxj2OGx03qF4D1qOyv4oNSvVCznq+8cfpz
+fP8AStj/AOxtla6VJDaXLXMDp580gwU5zvzAg7/agsXBN7pqWqWYYK4QOQTsexq4wjTrpArMvKw+
+1ZXw1oyxW8byOfPyc4bOKmJWvLRSPMbH5gcYxmoq5axacP2ulSAzx28e55om5Wyfl3qJ4Jt9Gtbt
+ruCGeaRsBGlkJyM5O9VaOG8v7oJzFhnY9Rg1oGh6BIlp5WQVC9em+KKstmq3isvLEI5MgqNwfXtW
+O6j4EcH2/Fd7qF811PBNKZorQNyRIDuVyNzv7jtU5xBxbxhoDfh7Hh6wEEPWV7gF3/2ov9a5oPE2
+rcUomoXUFvBaRgoAHLSM/cEY+ED077VUSem2NhpNmLLS7K3srdOkcKBR+nU+9CQk96OxBpN8YqVT
+dzSecjpSjgE7UFUBTtvQJMoAz61xRntSpAoBRisqIF36Cjcu3SukUBt1oOcu9CjKAaFYq6r2AcZI
+pxEvTem4SnMKYrUZPrUbipa2bG9RNqN6koelbglElBXBGR6Go/VLezNlPKmnW0kgQkc0S7n7UvET
+iofjLVrjS9PDWyKZHzueigdT+o+9VHmziHSiuu3d6UjJmdZ1B/KBJvnHrnI9iDUqAlxZLplxp6lF
+AdnXClhk7j164ovF/Pa6v5v4ZppJCWl5JCNic8uOg3Pak5+JntoRaS2DRyQrkCQbgEk9vcn70ZV/
+XtFktLCS4ihcRscDKbjpt/Or34IW8eraeLS4vrkJbyeYsAlbkLDcZXoaoGtahf3/ADT80nk8u4J+
+EbdKmPCrU7jh/VI71gTaSEeZj933+VFbglytnfGKdVUMxOMGpaSS3uYEBQZGQDmm2o2FvxBpy3tn
+MAWXmSRNwfqKqY1HUdKumt7wFcbFlG30qYrQOHohBdBlwA24wOm+f+vrSfGfF8nOdN0+8MKQjMrq
+uSx9B/aqmeJkS2llRsyKhKgnHNjNQcU97qzNGzRKZy3MScg/P5bUDbiHjp7GZFt9OlupWBBeQkkD
+tnHqMnFO/Crij8fxBLbRwGOKaIyEZ3LZzzH7kU2nsI9Jljt5ktnuZRhZMZ39fUbZH2rQ+H+GtN0x
+zeW9nDFdSKFkdR1wP0+lBNk5FEkyRSpGBik2HzqBHFGA964QAcZoZHTNWtDADHWuMpxkGuLgnFK4
+GOlZoR5cbmisTncUs4xtSRHM1AF+VCjqu2TQrNVAxg9KdQj1pJBvTmMHpWoyc2y5bYU+jUimtuu1
+PIx9a0HMewFR/E1g2oaXJChxJjCt6Zp+uwBpxFvjeqjzxfF9O1r8XeIwYebHKh/ccMcbf7Sp981A
+Xer8P3Mt3eXrPJMIisUUSgZboC3etw8W4OFE0sXWs31tYXiDML8vNJJ/p5Ruw/lXn6/01dXuGvtO
+tMO8uJQgPIpYZH3wftREUbt7pvwFkk0dtLgSK+Pi9/b9auOkaekdqEIxt27Va+A/CfWbuwTU4tPE
+kL5BmdwoGOu27Y+QNPf8hcSyxxhAiNy87HkUn5tj+9Ax4M1274duxDcRmfT2OSq9Y/cf2q7cQaVb
+a1apcWEsTLIAUYbA59ao2qXXDehoX1PU43cDaKE4P1JGfsPrTXhjiXUtUMknDn+W21ssn/kTO3O5
+9cbkfeinXEXDuraYhafSJJ0XOJIGz+lUK/4ku9PZ1tw6GPJIdcEe1bXJxWLfTHg1WExl15SVBIBP
+p9axbibSdT4g1aX/ACyyldJGCmRl5VC+pJoENB41upOIrK/1W1W8tYHBeHJGR6/Mdd69P6Ve2Wp6
+bBfafKJLaZOZGH9fevMs3DtnoFsJNTW6ZCcGeJQVQ+46/fFX3wr4ls9Im/Cxail1pc7ZYdHgb+Ir
+6euD70I2Vl+HNJkUvsyBlIKkZBBpJhRSLYwaKuD1FKkY61zAA6Vm0EQKW2pZeXuKSU4pQnbaoorh
+TSZ5RR96IwqDqnOcChRosAdaFZqocYB6UrG2/Sk1BzS8Y3rcQ7tj7U9i3ppboaF1r3D+l+bFf6xY
+296q/s4ZSTgnozKu+PatodalfWmm2Zu76dIYV/ebv7AdSfYVmPFnirO4ktOHoDD+7+JlALf8V7fX
+7VfOEtW1K0sNSng4j0Xi7VrlwbeGS9NmkSY/IkfKwXt8+5rJ9S401nT9TOleLPAyzo7HlvIIBDOo
+/iR1+GQD2OKuMqNftPqd1JPqM81xNJ+aSVizH61evCS+s40l0q5jhWYDy/2oGCMgxsc9gwAPtUzx
+F4ZTDQouJeGmm1TRp4xMvNHyzRoRnJHRh7j7Vmmr+ZYTW+q2p/aQNhwD+de6mojWPETxMvdE0KG1
+1LmFxFzJDaKRGWYE/E6rsFHQD296wHiDjbiTXJna71KZUb/8UR5Fx6bdfrTXiC9u9b1iW/uA3NId
+lySFHTApyugO9mLiHJI3xVVXzzN8TMSfc0406/u9OnE9pO8TjuO9WCLR47q3DACGdR3GVb2P96tX
+iN4K8Q8G8LR8QXV5YzxYX8RBEx5oS3pnZgDtQJaD4j3z2fk6laC5jGzSR/mX3q26JxClzah47kTI
+35SVwe+29UfwY0NNQur2/uA4ihVY0IPVzv8AI7Dv61aNStX03U5YT5fKeZ4+VcdOQ7j1xn7VApqL
+/wCZWNzbSr+zmjZcHsazC202WwnVortlukJ/J0BHbPetK06QvEzEHIkI+hFI3fh1+O4e1Pia31uO
+G5t5ARZsm7DAyQc5/Sg0Dwd4ztdU4Y/C6jMyX1rJycnLn4NsfYnGPf2q6aVq2m6tC02n3ccyI5jf
+BwUYdQwO4Psa8kcLa3LpGssWeTyXfEgU4Jwa2vWLxdQ4e1oWGnabc6Zcwfio721YJcQuF5v2i7MQ
+G2BGwzvRZWsOuKTIIqgeC/FjaxpB0q+maS+tF+FnOWkj9fmOn2rQGO9ZsUl0augkVwjfOKAB9qiu
+52pNmzttRiDRGU5xQGQ9xQrqKcbihWaIxF70snWklB74paJcmtQRXHHEq8NaA90ih7uU8lup9cbs
+fYf2rDLvWJ9QvGur6WWWd2y7Mc5q9eOV60Op6XbqhYLA8jHtu2P6VQYruAkeZFsepxW2KfQTI35G
+Gau/DvG99aWo0zW401vR2xz211livujHdSO3p7VSLdrGX9/lPTI9KXaCXk/ZOjemaDW9M02NpbTi
+LT/FjVrPhaxbzf8AL7mTnmgYDaH4iQVxtuDt69aoviLrXD+u67LcaLpE1pBJnzWcgLMf4gmAVPr2
+/nVMnlBcRXCBJ13QkbA06065FxCWcYdG5WT0IoGI0eISMgUe2RR9OJt5Ws3xjtt1qUB518zG6HtU
+frScskVygIwd8UHZ7copUCjaxe8bcR6Yuj3V7NeW5OUiCc0jY3+ZG2aXibmUSb4YdRU3wbxqvA2q
+zak1i9z5kBjDkcxjOf5f2FTQXwqnWz4eOmvGY5orhxICuG5tuvv2ol9rcOs69d+UcpZOsRYfvFld
+f54qNtOJ5+Jtf1XVpMwySzI/L3/Ly/8A1FHg0aLS9aklslKw3flM8Z3AbzR09tzVEjYcoeULsGAc
+CmvEOnarqt1aWelreSySK3PBbgnnUYO4HYUdJkSWAKd+XlOPepJ+J9S4VhttZ01Od1k5JEJ/OpBy
+PuB9hQZLxHpk+n6sySQvE2fiR1wVYdQQad2F5eWkb/h5XiJjaNgDsVbZhj3qa4o1W64nu5tVvIDF
+M7c2CcmohlBJ2xlaB9wzrl1w/r8Go2ZUyxDHK35WB6g16R4T1634i0SHUYF8st8MkZOeRh1FeW/J
+WRw2enU1qvgnxTp+nR3Gj6lMtuZ5Q8EjbKSRjlJ7dBUpK2QkD0rg3zRGIJ2O1GSstlFwAc0VsA7V
+35UXBzUo6p2oV0KaFZVGKm9LxKM9M0T0NdklENvLLjPIhYD5CukRjfi1qCXHF88eQY7ZFgH2yf1J
++1VyGOORMYGKbNK97JPLcMWkkcyOT3J60haieGQqmeXO2a0wkJ7FGHwkKexBpBLqeycJOTy9m7U/
+tJo5sebGMinM8VpLGedMqN996COvF/Hw+ZFyyOo6A9fl71H2NwEvg2cGQcrjpuOmf+u1PxZRK7SW
+NyFx+5UfqyK3/eEBW4jPM4/jH96Im7d/jOGG/auXo57RlO/ptTHS7xJ4lIOT0p60mQ4GxopppdwG
+QwOfy7UtIshlFuInmD4AULnOegqOGI7wMo2JwfSn8mXQgNysR8LZ70Fp4h8ML/hWK01C51Czb8eq
+q0ETEtGx3HsR7im96wjWJFlVmjKq3fmxlz/8ahtK1TXdc4jjt9cvGljit2EQXbmIAAz9KXnYWVq8
+apjCSSDfJGwTr/yNAiMlFy/7oORvjepDihUk0CGFZFjDToDI/wCUZOMn7060vT7OXQVvpLadCwZY
+wzBEkPL8JJPQBs5OcHp61E8Xs3+TwW867mQcy47gGgl+P+CtL4d0qyv9L1w6glwgLq6gEZHUY/lv
+Wb3MmG5c4PKR9akYbaRinPczyRpuiO2QufTNRuqxNHdKvYsD/wBfagPFtEAegFFkbzcKq7A9aDDK
+igx8qHb8zHCiiPQfhJrX+bcJQiWQyT2rGFyeu3Q/aropX0rzp4e8YS8JXqxPCJ7O5I89R+ZcfvCv
+QOnXttqFlFeWkgkglUMjDuKzY1DvPegGoLRWqNFFbI7UK4g2O1CshgAcelGRFkUxt0YEH610jAo0
+LKD71qDzaI1g1Ke1JB5XZM/I4o1xGY2Db4FLcWRfgOML9OgS8kH0LH+4p75AmgDcwwa2wYYLplNi
+BtiixzzRH4gWA7EVyM/h5miz8NPIRHMOUAADvnFENpYrO7X87wSnoU/tTC80bUoR5kEgvUG+xww+
+h/pUxJawr8cec02FxJE27HlHrQQOkTG2lliZCuGyA2xHtUxFKzfER9qYaxBLPci9tD5g5cSRj8w9
+x6ijW8+YsZHTegPLvuMDBp2JAIlbbcY6U2tpPMV8/LOKAceXjB2oJ3hCeN9Z3UMywvjbp0rt9bmS
+OeeQZeQQrGN8AkFjt26ioLhed4uKIU5iOdXBH/GrLqDfEkaDc3BJ/wCKqv8ASirgbmxtLENZ6W7x
+ae37cMQFYc+Qq8wOSAASfXvvVP8AEdndhLJzeZ+LfmBbJyc7VJPdW0ziW6hilkAzzFFBHyqJ8RLt
+brTLec8gkacFiFwT8JoIK2lXlBYbVE8RMFaKT19/ejG5wAq0x1uVnhiB3IbagdwqHVc9MUSP9vdF
+v3E+FaPptvdX7Q2NhDJPdzEJHGgySTWq2/hOukcMvd6teSC/EZZUhwY1OM4JxknttjrQZqYlDGRx
+sOgNaX4M8Sw2Usmi6hOscUp5rcucAN3X60Xh3wf4k1e1ju7ua105GHMsUuWk/wCQH5flnNUnUrFo
+NVms5MB7VzGeX+JTg4+1QemVwRkYxRGJzWd+H3HtrJDBo+sytDdKOSOdz8MnoCexrRRg4IIIPTFZ
+aHRtutCiEkChWappJkLjJrkaEnJo8rCgjYxvW4MN8XrNYeNr3GR5ipJ09VFRWg3XnQlMnmTYg1dP
+G+z/APGbO8HSa25D81J/oRWXQztYXvm78j7PVYqw6hFGSJOXp1OKbpzKOZExipGIxXduGT4sjam3
+lmJzG+du4NULW84YBZCPfaiXtosykxt13xXFCEEDOR39aNDKUk/KceuelEVq/gu4JcRrICD13okr
+XBhE8x+IHDEdSOxNW55oHT4lXPqKjr+2je0kjHLlxjtRURZNiPYjfvXWbAIYCkbZuVeQnp1HvSU8
+yDIBG+2KIW055DxDp7W7AP5uCcA/Dj4v0zVnhdpZIWOxMZkP/Ni230IqrcMRo2tyTqCTDbyMBnbJ
+HKP/AJVaLGRBcXDkhkiYRqADsFGB/KosSyxQOg50JPuKqvH7CJrG2RsjDyN+gFS9veTT3W4+HPrV
+X48uObWljBwI4gMDsTRTbhvTbjXtfstHtZooprqURh5Gwq+5qa8ZeCLjgi6s7d9VttRSUkiSIcpV
+h1BXP61VrW4ltikttIYZo2DI69Qw70e9utS1/VrSPULlp5ZZVXJwOpAJrSN7/wAO3B6Wul/9qL5R
++JuU5bcMu6Reo92/lWo2GmS6hqBv78AJGSLe3JyFH8Te/f2+dNOHEkt+FtOtRcRmNY1AePBGw2pT
+U9RFoHSW9htI+txcu3KIl74z1JOwHr8qjUTkY/Fo0Vs7RW6bM46k98e9Y54+No6alYWtjbRw3MMZ
+82RVwFQ9Ax9Sd9/ep3WvFzQ7G3FhollcXap8IkJ8tSPXfcn3xVP408QdP4k4Yk0M6GLbmkEvmGfn
+JcHOTsPehqhpb2kjiTl53H72dqv/AADxs9hyaZquTaDaObmyY/Y+1Z000iAsRGF6DkJOPnSE1xME
+ODzKR2rNiPTqyxTQrLDIskbDKspyCKFZ14IX7XGgXFm0vP5MpIBOcA9PpQrNaXlmwBtXRkkGkQwz
+SqZyKSireMVh+I4TivAPitJ1JP8ApbY/ry1h+pW3OTgnHbevQPHsN3qOjwaDYcguNTnEILdAoBcn
+/wBtYMRzKFIww2atxmmGl6nPps3lOeaPO4qz2t5aXyghwD71WLy18zJC71FwzSW8uCSKrK+XFpyn
+nifJPXemp8xdgjZ+RqOtLucqG5+YEbUs2pyoCWJoHEqyn8sbkkdlNRV4uoKwPkuvtykVIwcQRK2G
+yKPNrEEzpySFGXpk/Cw9DRVT1qeW1lWQoyCUZx03HWo1LtpG3JrdvBySz1fxAtbDUeG7PUomVjI8
+8SukAxs45ts5GPXrXp+z4b4chAaHQ9MjI6FbVB/SkMeG/CzTdS1jiRNOtdPu5TduiGZISyRKrcxZ
+j6bV6l4a8FuDLSxWO7hvb+U7vJNM0ZLHr8KYx+taqkcUIVI41VewUYArsigSZGKuKoKeEHAMJDpo
+rhvX8VKf5tTW78DfDW+na5udDleV8Zb8ZMOnyatLyCu/pQQgDqKDNbPwJ8M7W6juU4e8xozkLLcy
+un1Utg/WlX8EfDsasurW+hm3u0bmUxXEiqp9lzj9K0gkAda7nK7VRVZeDdIeJwqzwzH8s6vzPGfU
+Bsr+lecPGPh/ifh/iBbLW9Sl1GxkzJY3LfCGGdwR0DDv8/evWb7HOdqpvjLwsvGHh9qGnxIDf26G
+4sX7iVRnl/5br9amFeQiOQ4yCPbrTeSLLl4zv1371DQX8yA+YCGHX2NBuJCshR0UgbdKiHdxd8jl
+dwfSm348xnm5Oai/ibe/bflRj0P96ZXHNBLyMpA96GtG8G9cS34rWDIWO9Qxuv8AqG6n+n1oVV/D
+S3a7490qNCwVZfNbHooJ/pQrnZjUejBjPSl12IG1Nhkt0pxGD1NSVcQ3F17HpeqcOalNJyRRaj5b
+tnAAdGWsIlYJql7CSMpcOvX0Y1vvG3DZ4q4Yn0qO4W3nLLJDKwyEZTnt7ZFYxxfwJq3DV9LNNfR6
+j+xWeeRFK8nM3IOvXfG9bjNRbJzHAxnGMUwurJJVbb608t5gFywJJ9KcZXBBGRVREabM1rP5E26n
+cGp0Q288PMNjTC/tkki5kwCB2613SblsCF2APT50AvNMj5TIuB3z3qNSMBvi7Hap65TbP9dqirhV
+QFsYOelNMW7wn1ltC4uspjPy28kgSTmOwB6H23r2Fpl+rwhieY8ucj+9fP1794mJBAxWhcEeJHF1
+vYCCDU3Fra/xgNk9lyRnHtRXtA3S8iuCppvd38ceDg7HtWD6B40W7Wwe9tQZlHxRBsBv9p7VcND4
+80ni+ynbTGkt7222ntJtnXPQjsw9xV1V8XX7U8o5XO5H8v70dNcsOflZ+VvQmszvG1Yc7RJzxk5y
+N+tIQX8sbjmkhBO551II+9T9GNfTUbaTHJMjfI04S4UjGay+zvp5CpJBP+kbCp2y1GRByFmPfc9K
+foxb2lDE7/ShDIomUZ2OxHzquG8nZQyinNpdyNgEYYGmmPH3jLoFpo3iVrulEGBBcmaEqNuST4wP
+pzY+lUu60i2EBImVpPbvWwf4z7Q23iBpeoRLgXenAE+pR2/owrCo7iUOAT9KrIvJJbyfDkYNSkFx
+Hd24jlxzr+U0QeVdDHMFIG5NMJk8p/gl+1X0aj4Eaf5vFNzeFdrW2IB92IA/QGhVn8BdNntOF59U
+uYyn46UeVnqyLtn5Ek/ahXLq/Wo0LlAbY0dAc9TSJOD70ojjY1iNU7iyM4yKqfF8IvNa1KyfcT8N
+3DKD3aN1dftirVDKp9Kz/wAbLq60pdO1ywmWN+SazfftIvp8s1uJWNluQq/NsRnrTtJkbBUjGKj7
+OSOeE2rN8QGVNNj51jNljzRnvW2U7JKrKQoyR1phM/lSK64z1NKQOsq8/MObrmkJjzty8h9M1BOW
+E63UIQMOalZdBkul+J8DtvVTE01pLzwyMuPQU4bi3UUQJkEjbJFMNSl1wzbwjnurxQvcLTC61OGO
+FdO08IkS+nf5moDUtbvr5irynHtSNoxiIbO9WRFnVWEBfm3A9abadJxE16l3pN7cWk8eyyxyFDj0
+yO3tSNpqSD4ZcYPWp231e0t7blgYA43FBYLbxG474Zt+eW/ttULYDrOhJP1BGftVguOK+MNV4cte
+I7LheG4sp8q01rO+Y5FPxKy9j36dMVlVzfSX1zsMhdzWm/4fePLPhniSTh7XJUj0bWCFLufggnH5
+XPoD+Un5HtRZVq4J8Q+GDpbza/FrMFzAc3Kxwq/kL/ER1K57gbbVpPDHFPAuuMo0jjbT5XbcQzv5
+T/8Apf8AtTTirwj0niC9N7a6oulXirmKSABiT/qGd1x2715q8SOAZ9D4gm0y4jWw1INlMHFtdL2a
+Mn8pP8JqZFe1raxkKjlaGZT3RhinSWLKdosZr532mu8UcPXLQ2uralp8sZwViuHT+Rqaj8XPEeKE
+xJxfqnKRjeXJ+53rWJrYv8cd9YC94ask5Wv44pXch90jJUAEe5B+1eaDKSMgkGlNV1HUNVvZL3Ur
+ye7uZDl5ZnLMfqaluDODeIeLLvydHsHkjU4kuH+GKP5sf5DJ9qvgghI4YkM2/vWreEXhhfcQSxaz
+r8Ulto6kMkbfC917DuF9+/b1GkcB+EPDvDix3mphdX1Jfi5pF/Yxn/Snf5tn5Cr9PP22A6Vi9Lhp
+NHFEkcEEaxRRqFRFGAoHQAUKTnkHPmhXOqTcnNBGNChWI0VjbvgVnX+IiMtwbZzKQDHeA/dWoUK6
+c+s1g0N2/mrMuzKas0YS/tQ7LguvN8qFCt1kwV2tJ/KU5XNO2cqSQBkj0oUKBheFmzvudyah51Oe
+tChViVyOFQfenCxKRvQoVUJSRgHrSTMQ2ATQoVYsOIZ3gBAJpve3Ek5BJ70KFIJXh/i/ibQZEl0r
+Wbu2KkEKJCV/9J2q73Pi3xPxRZ/5JqENhNPe8tsLqSLLICcbD60KFLCM+4gDw63cWUkjTpaSNApb
+qQpIrWPD/wAMeG+IuDLW/vWvYbqUNl4ZRjrtsQaFCs9XIs9XHhvwa4M04CS9hudVl5sg3EnKg/4r
+jP1zWhW8dvY2kdpZW8NtbxjCRRIFVR7AUKFc9tawSSViDTSRsnehQqKbyjLbUKFCs0j/2Q==
+
+Cheerio!
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1--
--- /dev/null
+Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail
+X-newsreader: xrn 9.03-beta-14-64bit
+Sender: jimbo@lews (Jimbo Foobarcuux)
+From: jimbo@slp53.sl.home (Jimbo Foobarcuux)
+Reply-To: slp53@pacbell.net
+Subject: Re: Are writes "atomic" to readers of the file?
+Newsgroups: comp.unix.programmer
+References: <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>
+Organization: UseNetServer - www.usenetserver.com
+X-Complaints-To: abuse@usenetserver.com
+Message-ID: <oktdp.42997$Te.22361@news.usenetserver.com>
+Date: 08 Mar 2011 17:04:20 GMT
+Lines: 27
+Xref: uutiset.elisa.fi comp.unix.programmer:181736
+
+John Denver <jd@clare.See-My-Signature.invalid> 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
--- /dev/null
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "John Milton" <jm@example.com>
+Subject: Fere libenter homines id quod volunt credunt
+To: "Julius Caesar" <jc@example.com>
+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.
--- /dev/null
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Socrates" <soc@example.com>
+Subject: cool stuff
+To: "Alcibiades" <alki@example.com>
+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
--- /dev/null
+From: Napoleon Bonaparte <nb@example.com>
+To: Edmond =?UTF-8?B?RGFudMOocw==?= <ed@example.com>
+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.
--- /dev/null
+Return-Path: <foo@example.com>
+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" ?= <oetzi@web.de>
+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<!!L)l!!%_I!!
+X-Spam-Checker-Version: SpamAssassin 3.0.2 (2004-11-16) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.3 required=3.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.0.2
+
+Viele liebe Gruesse aus der Stadt der Städte..
+
+__________________________________________________________
+Mit WEB.DE FreePhone mit hoechster Qualitaet ab 0 Ct./Min.
+weltweit telefonieren! http://freephone.web.de/?mc=021201
+
+
--- /dev/null
+Date: Mon, 13 Jun 2011 14:57:25 -0400
+From: xyz@123.xx
+Subject: abc
+To: foo@bar.cx
+Message-id: <abc@def>
+
+123
--- /dev/null
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Geoff Tate" <jeff@example.com>
+Subject: eyes of a stranger
+To: "Enrico Fermi" <enrico@example.com>
+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
--- /dev/null
+Date: Sat, 12 Nov 2011 12:06:23 -0400
+From: "Richard P. Feynman" <rpf@example.com>
+Subject: atoms
+To: "Democritus" <demo@example.com>
+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.
--- /dev/null
+Return-Path: <foo@example.com>
+Subject: Fwd: rfc822
+From: foobar <foo@example.com>
+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: <cuux@example.com>
+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--
--- /dev/null
+From: dwarf@siblings.net
+To: root@eruditorum.org
+Subject: Fwd: test abc
+References: <8639ddr9wu.fsf@cthulhu.djcbsoftware>
+User-agent: mu 0.98pre; emacs 24.0.91.9
+Date: Thu, 24 Nov 2011 14:24:00 +0200
+Message-ID: <861usxr9nj.fsf@cthulhu.djcbsoftware>
+Content-Type: multipart/mixed; boundary="=-=-="
+MIME-Version: 1.0
+
+--=-=-=
+Content-Type: text/plain
+
+Saw the website. Am willing to stipulate that you are not RIST 9E03. Suspect
+that you are the Dentist, who yearns for honest exchange of views. Anonymous,
+digitally signed e-mail is the only safe vehicle for same.
+
+If you want me to believe you are not the Dentist, provide plausible
+explanation for your question regarding why we are building the Crypt.
+
+Yours truly,
+
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: inline; filename=
+ "1322137188_3.11919.foo:2,S"
+Content-Description: rfc822
+
+From: dwarf@siblings.net
+To: root@eruditorum.org
+Subject: test abc
+User-agent: mu 0.98pre; emacs 24.0.91.9
+Date: Thu, 24 Nov 2011 14:18:25 +0200
+Message-ID: <8639ddr9wu.fsf@cthulhu.djcbsoftware>
+Content-Type: text/plain
+MIME-Version: 1.0
+
+As I stepped on this unknown middle-aged Filipina's feet during an ill-advised
+ballroom dancing foray, she leaned close to me and uttered some latitude and
+longitude figures with a conspicuously large number of significant digits of
+precision, implying a maximum positional error on the order of the size of a
+dinner plate. Gosh, was I ever curious!
+
+--=-=-=--
--- /dev/null
+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
+
+
--- /dev/null
+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.
+
--- /dev/null
+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
+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<?"!%LG"!cAK"!_j(#!
+Content-Length: 1879
+
+Test 123.
--- /dev/null
+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
+
+
+Let's write some fünkÿ text
+using umlauts.
+
+Foo.
--- /dev/null
+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.
+
+
--- /dev/null
+Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail
+X-newsreader: xrn 9.03-beta-14-64bit
+Sender: jimbo@lews (Jimbo Foobarcuux)
+From: jimbo@slp53.sl.home (Jimbo Foobarcuux)
+Reply-To: slp53@pacbell.net
+Subject: Re: Are writes "atomic" to readers of the file?
+Newsgroups: comp.unix.programmer
+References: <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>
+Organization: UseNetServer - www.usenetserver.com
+X-Complaints-To: abuse@usenetserver.com
+Message-ID: <oktdp.42997$Te.22361@news.usenetserver.com>
+Date: 08 Mar 2011 17:04:20 GMT
+Lines: 27
+Xref: uutiset.elisa.fi comp.unix.programmer:181736
+
+John Denver <jd@clare.See-My-Signature.invalid> 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
--- /dev/null
+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: <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-----
+--=-=-=--
+
--- /dev/null
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "John Milton" <jm@example.com>
+Subject: Fere libenter homines id quod volunt credunt
+To: "Julius Caesar" <jc@example.com>
+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.
--- /dev/null
+From: Sitting Bull <sb@example.com>
+To: George Custer <gac@example.com>
+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
+
+/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
+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
+2A7BR0UDoKuJaud54gcYX0Jk/wC46Lbno5T4sf8ALJ/Sqlq+sx3j/wDiOoahq7A5HmSFIwfYf2qA
+uLxpn55ZGkf1Y5ptLPynAxVxEsdQXcwWNrCvuvM33phM8jS+Z5rZBz1pn+JxuWxRGvxzbDI9aYbE
+rHfXgGFuX/T+1OrPXdZs3LWt/NCx6lTjP2xUGl3EO9HN3Gds/ah8XnTvEjiiywZLsXKj92UBs/ff
+9at+heL9jNiPV7FoG7vCdvsf71izzAocGkWkyD1phuPVuh63o+uRc+mX8NwQMlAcOPmp3p8UA7V5
+Gs725sp0mtLiSGRDlWRiCD7EVq3Afi5KrR2HFA8yPoLxB8S/7x+8Pcb/ADqYNfkA3pLlpWKWC6t4
+7m1lSaGReZJEbKsD3BoKtYWEwvSjhQe1GA9aOoFA2K4JoAb9Kdcq5zQEagnYVKpALkHrQpyAB0Az
+6UKioTlIpxCMikyMiloU2qwLRfMU6iGe4puin0p3Ah6kYHf2rUQsgVFLuyqoGSScACsj8TfFURPJ
+pXDEo2+GW8Hf2T29/t61D+MHiM+pTy6BoMpFhGeWedT/AOeR2B/h/n8qyiSQjuc961ImndzdzTyN
+NPK0sjHJLHNNZLk5601klY96SZqrOnRuipJApKWdn70iuTTqztWnkUAEg+gqp9JRrJIQEVmPsM0s
+LG9dsC2lyenwmtO4D4PZ3jmKeYp64GQPnWoDw8WeBZIrZUbqMj+dTVx5l/yvUACfw0gx1yKQaOaM
+kNGy/MV6UuuALhIyGRDj2qj8W8E3do3mCDIJ3wOgppjICzD2owmPepnWdKaAtlSCO2KgGBBwe1VM
+OBJnYUA2BTdTR1zQaD4UceXHDd+thfyNJpE74dTv5JP76+3qO/zr0PHiQB42DKwyrA5BHrXjtc16
+l8Lo75OBNIF+GE34cYDdeTJ5P/bisdRqLGq980cLjpXQpB60dQc1lonymuBd8UsFocveoogXahSm
+2+1CsqglI6E05twu1NU69KdW/XFaiHcYXOKznx44xOi6QvD2ny8t7fITOyneOE7Y+bbj5A+orRlK
+Rq0kjBUQFmJ6ADqa8l8ba5LxDxTf6vITi4lJjH8KDZR9ABW+WUY03KMCkC5cnJorHJoA4rQ6elBI
+2kOFGTjNGUcx32qZsdAubiESoC+eyjcUQwgspVTmZod+ilxk1a+EtC8/UIeZ+WOVSSmNw3/WKjG0
+60tpbeCZJTLIQVKHIO/Tb5Vb7Vbhbxr+1jES82Vjkckuo/d6bHHr7VBtXhrp8ttaxhreGKFfzcw/
+X1rTbdomQBQCpGxA61ROFW/EaBDcsQrcgyo3GcdKnLNTHmWV0VTjdTjb5f8A+UaieFjHK/mHBQZJ
+J6Yqr8XXPD8UDie9tkC5BzKPsKrfGnFGu6jM2k8OlxynlaY/EgPYbd/sKyjTOFeK+JeJmtdUSRgk
+gDM/wDrjI9fuaiadcY22k3EL3MbRgcpCuB136VjOoIou2ZNlJyK9NeKfhUR4fwS6aZDLYEtcIDu4
+I3b6YrzJdczTNzdtvtViEQoNKwxs7qiKWZjgADJJqS4b4e1fiC/Wy0ewmu5m68i/Co9WPQD3NejP
+C7wtseEuTU9UaK+1gj4SBmO3/wBuerf6vtVXFO8K/CR08nXOKoimCHgsGG59DJ//AD9/Sti5eu1O
+5zzHem/b3rPqyCgHNGUUXau5qK6a4a4DXcZqApOxwDQoxWhWK0gsdDinEGAelJMgxS0CgVqMmHHM
+7wcDa3LHs4sJsEdvgIryU29ez0tobmNre4iSWGVSkiMMhlIwQazXjHwGtLsyXfCt8LWQ7i0uCSny
+V+o+ufnXSMvPGN66BVl4m4H4p4clZdW0W7hQHaUJzxn5OuR+tV4Ieb8pG/pVE5w/odxcSiaWNhGm
+CB6g9/lWn6XprWmnk5jifPQ9MHPX5AVQtK1trTTrlDkyOnlg+m2/9BTF77W7iN3Ms5UAFiW6elRF
+6gsNQZ31aOS3gnjCrIzDnJU9cdh9KXKZto52hYWkcgAxj2OGx03qF4D1qOyv4oNSvVCznq+8cfpz
+fP8AStj/AOxtla6VJDaXLXMDp580gwU5zvzAg7/agsXBN7pqWqWYYK4QOQTsexq4wjTrpArMvKw+
+1ZXw1oyxW8byOfPyc4bOKmJWvLRSPMbH5gcYxmoq5axacP2ulSAzx28e55om5Wyfl3qJ4Jt9Gtbt
+ruCGeaRsBGlkJyM5O9VaOG8v7oJzFhnY9Rg1oGh6BIlp5WQVC9em+KKstmq3isvLEI5MgqNwfXtW
+O6j4EcH2/Fd7qF811PBNKZorQNyRIDuVyNzv7jtU5xBxbxhoDfh7Hh6wEEPWV7gF3/2ov9a5oPE2
+rcUomoXUFvBaRgoAHLSM/cEY+ED077VUSem2NhpNmLLS7K3srdOkcKBR+nU+9CQk96OxBpN8YqVT
+dzSecjpSjgE7UFUBTtvQJMoAz61xRntSpAoBRisqIF36Cjcu3SukUBt1oOcu9CjKAaFYq6r2AcZI
+pxEvTem4SnMKYrUZPrUbipa2bG9RNqN6koelbglElBXBGR6Go/VLezNlPKmnW0kgQkc0S7n7UvET
+iofjLVrjS9PDWyKZHzueigdT+o+9VHmziHSiuu3d6UjJmdZ1B/KBJvnHrnI9iDUqAlxZLplxp6lF
+AdnXClhk7j164ovF/Pa6v5v4ZppJCWl5JCNic8uOg3Pak5+JntoRaS2DRyQrkCQbgEk9vcn70ZV/
+XtFktLCS4ihcRscDKbjpt/Or34IW8eraeLS4vrkJbyeYsAlbkLDcZXoaoGtahf3/ADT80nk8u4J+
+EbdKmPCrU7jh/VI71gTaSEeZj933+VFbglytnfGKdVUMxOMGpaSS3uYEBQZGQDmm2o2FvxBpy3tn
+MAWXmSRNwfqKqY1HUdKumt7wFcbFlG30qYrQOHohBdBlwA24wOm+f+vrSfGfF8nOdN0+8MKQjMrq
+uSx9B/aqmeJkS2llRsyKhKgnHNjNQcU97qzNGzRKZy3MScg/P5bUDbiHjp7GZFt9OlupWBBeQkkD
+tnHqMnFO/Crij8fxBLbRwGOKaIyEZ3LZzzH7kU2nsI9Jljt5ktnuZRhZMZ39fUbZH2rQ+H+GtN0x
+zeW9nDFdSKFkdR1wP0+lBNk5FEkyRSpGBik2HzqBHFGA964QAcZoZHTNWtDADHWuMpxkGuLgnFK4
+GOlZoR5cbmisTncUs4xtSRHM1AF+VCjqu2TQrNVAxg9KdQj1pJBvTmMHpWoyc2y5bYU+jUimtuu1
+PIx9a0HMewFR/E1g2oaXJChxJjCt6Zp+uwBpxFvjeqjzxfF9O1r8XeIwYebHKh/ccMcbf7Sp981A
+Xer8P3Mt3eXrPJMIisUUSgZboC3etw8W4OFE0sXWs31tYXiDML8vNJJ/p5Ruw/lXn6/01dXuGvtO
+tMO8uJQgPIpYZH3wftREUbt7pvwFkk0dtLgSK+Pi9/b9auOkaekdqEIxt27Va+A/CfWbuwTU4tPE
+kL5BmdwoGOu27Y+QNPf8hcSyxxhAiNy87HkUn5tj+9Ax4M1274duxDcRmfT2OSq9Y/cf2q7cQaVb
+a1apcWEsTLIAUYbA59ao2qXXDehoX1PU43cDaKE4P1JGfsPrTXhjiXUtUMknDn+W21ssn/kTO3O5
+9cbkfeinXEXDuraYhafSJJ0XOJIGz+lUK/4ku9PZ1tw6GPJIdcEe1bXJxWLfTHg1WExl15SVBIBP
+p9axbibSdT4g1aX/ACyyldJGCmRl5VC+pJoENB41upOIrK/1W1W8tYHBeHJGR6/Mdd69P6Ve2Wp6
+bBfafKJLaZOZGH9fevMs3DtnoFsJNTW6ZCcGeJQVQ+46/fFX3wr4ls9Im/Cxail1pc7ZYdHgb+Ir
+6euD70I2Vl+HNJkUvsyBlIKkZBBpJhRSLYwaKuD1FKkY61zAA6Vm0EQKW2pZeXuKSU4pQnbaoorh
+TSZ5RR96IwqDqnOcChRosAdaFZqocYB6UrG2/Sk1BzS8Y3rcQ7tj7U9i3ppboaF1r3D+l+bFf6xY
+296q/s4ZSTgnozKu+PatodalfWmm2Zu76dIYV/ebv7AdSfYVmPFnirO4ktOHoDD+7+JlALf8V7fX
+7VfOEtW1K0sNSng4j0Xi7VrlwbeGS9NmkSY/IkfKwXt8+5rJ9S401nT9TOleLPAyzo7HlvIIBDOo
+/iR1+GQD2OKuMqNftPqd1JPqM81xNJ+aSVizH61evCS+s40l0q5jhWYDy/2oGCMgxsc9gwAPtUzx
+F4ZTDQouJeGmm1TRp4xMvNHyzRoRnJHRh7j7Vmmr+ZYTW+q2p/aQNhwD+de6mojWPETxMvdE0KG1
+1LmFxFzJDaKRGWYE/E6rsFHQD296wHiDjbiTXJna71KZUb/8UR5Fx6bdfrTXiC9u9b1iW/uA3NId
+lySFHTApyugO9mLiHJI3xVVXzzN8TMSfc0406/u9OnE9pO8TjuO9WCLR47q3DACGdR3GVb2P96tX
+iN4K8Q8G8LR8QXV5YzxYX8RBEx5oS3pnZgDtQJaD4j3z2fk6laC5jGzSR/mX3q26JxClzah47kTI
+35SVwe+29UfwY0NNQur2/uA4ihVY0IPVzv8AI7Dv61aNStX03U5YT5fKeZ4+VcdOQ7j1xn7VApqL
+/wCZWNzbSr+zmjZcHsazC202WwnVortlukJ/J0BHbPetK06QvEzEHIkI+hFI3fh1+O4e1Pia31uO
+G5t5ARZsm7DAyQc5/Sg0Dwd4ztdU4Y/C6jMyX1rJycnLn4NsfYnGPf2q6aVq2m6tC02n3ccyI5jf
+BwUYdQwO4Psa8kcLa3LpGssWeTyXfEgU4Jwa2vWLxdQ4e1oWGnabc6Zcwfio721YJcQuF5v2i7MQ
+G2BGwzvRZWsOuKTIIqgeC/FjaxpB0q+maS+tF+FnOWkj9fmOn2rQGO9ZsUl0augkVwjfOKAB9qiu
+52pNmzttRiDRGU5xQGQ9xQrqKcbihWaIxF70snWklB74paJcmtQRXHHEq8NaA90ih7uU8lup9cbs
+fYf2rDLvWJ9QvGur6WWWd2y7Mc5q9eOV60Op6XbqhYLA8jHtu2P6VQYruAkeZFsepxW2KfQTI35G
+Gau/DvG99aWo0zW401vR2xz211livujHdSO3p7VSLdrGX9/lPTI9KXaCXk/ZOjemaDW9M02NpbTi
+LT/FjVrPhaxbzf8AL7mTnmgYDaH4iQVxtuDt69aoviLrXD+u67LcaLpE1pBJnzWcgLMf4gmAVPr2
+/nVMnlBcRXCBJ13QkbA06065FxCWcYdG5WT0IoGI0eISMgUe2RR9OJt5Ws3xjtt1qUB518zG6HtU
+frScskVygIwd8UHZ7copUCjaxe8bcR6Yuj3V7NeW5OUiCc0jY3+ZG2aXibmUSb4YdRU3wbxqvA2q
+zak1i9z5kBjDkcxjOf5f2FTQXwqnWz4eOmvGY5orhxICuG5tuvv2ol9rcOs69d+UcpZOsRYfvFld
+f54qNtOJ5+Jtf1XVpMwySzI/L3/Ly/8A1FHg0aLS9aklslKw3flM8Z3AbzR09tzVEjYcoeULsGAc
+CmvEOnarqt1aWelreSySK3PBbgnnUYO4HYUdJkSWAKd+XlOPepJ+J9S4VhttZ01Od1k5JEJ/OpBy
+PuB9hQZLxHpk+n6sySQvE2fiR1wVYdQQad2F5eWkb/h5XiJjaNgDsVbZhj3qa4o1W64nu5tVvIDF
+M7c2CcmohlBJ2xlaB9wzrl1w/r8Go2ZUyxDHK35WB6g16R4T1634i0SHUYF8st8MkZOeRh1FeW/J
+WRw2enU1qvgnxTp+nR3Gj6lMtuZ5Q8EjbKSRjlJ7dBUpK2QkD0rg3zRGIJ2O1GSstlFwAc0VsA7V
+35UXBzUo6p2oV0KaFZVGKm9LxKM9M0T0NdklENvLLjPIhYD5CukRjfi1qCXHF88eQY7ZFgH2yf1J
++1VyGOORMYGKbNK97JPLcMWkkcyOT3J60haieGQqmeXO2a0wkJ7FGHwkKexBpBLqeycJOTy9m7U/
+tJo5sebGMinM8VpLGedMqN996COvF/Hw+ZFyyOo6A9fl71H2NwEvg2cGQcrjpuOmf+u1PxZRK7SW
+NyFx+5UfqyK3/eEBW4jPM4/jH96Im7d/jOGG/auXo57RlO/ptTHS7xJ4lIOT0p60mQ4GxopppdwG
+QwOfy7UtIshlFuInmD4AULnOegqOGI7wMo2JwfSn8mXQgNysR8LZ70Fp4h8ML/hWK01C51Czb8eq
+q0ETEtGx3HsR7im96wjWJFlVmjKq3fmxlz/8ahtK1TXdc4jjt9cvGljit2EQXbmIAAz9KXnYWVq8
+apjCSSDfJGwTr/yNAiMlFy/7oORvjepDihUk0CGFZFjDToDI/wCUZOMn7060vT7OXQVvpLadCwZY
+wzBEkPL8JJPQBs5OcHp61E8Xs3+TwW867mQcy47gGgl+P+CtL4d0qyv9L1w6glwgLq6gEZHUY/lv
+Wb3MmG5c4PKR9akYbaRinPczyRpuiO2QufTNRuqxNHdKvYsD/wBfagPFtEAegFFkbzcKq7A9aDDK
+igx8qHb8zHCiiPQfhJrX+bcJQiWQyT2rGFyeu3Q/aropX0rzp4e8YS8JXqxPCJ7O5I89R+ZcfvCv
+QOnXttqFlFeWkgkglUMjDuKzY1DvPegGoLRWqNFFbI7UK4g2O1CshgAcelGRFkUxt0YEH610jAo0
+LKD71qDzaI1g1Ke1JB5XZM/I4o1xGY2Db4FLcWRfgOML9OgS8kH0LH+4p75AmgDcwwa2wYYLplNi
+BtiixzzRH4gWA7EVyM/h5miz8NPIRHMOUAADvnFENpYrO7X87wSnoU/tTC80bUoR5kEgvUG+xww+
+h/pUxJawr8cec02FxJE27HlHrQQOkTG2lliZCuGyA2xHtUxFKzfER9qYaxBLPci9tD5g5cSRj8w9
+x6ijW8+YsZHTegPLvuMDBp2JAIlbbcY6U2tpPMV8/LOKAceXjB2oJ3hCeN9Z3UMywvjbp0rt9bmS
+OeeQZeQQrGN8AkFjt26ioLhed4uKIU5iOdXBH/GrLqDfEkaDc3BJ/wCKqv8ASirgbmxtLENZ6W7x
+ae37cMQFYc+Qq8wOSAASfXvvVP8AEdndhLJzeZ+LfmBbJyc7VJPdW0ziW6hilkAzzFFBHyqJ8RLt
+brTLec8gkacFiFwT8JoIK2lXlBYbVE8RMFaKT19/ejG5wAq0x1uVnhiB3IbagdwqHVc9MUSP9vdF
+v3E+FaPptvdX7Q2NhDJPdzEJHGgySTWq2/hOukcMvd6teSC/EZZUhwY1OM4JxknttjrQZqYlDGRx
+sOgNaX4M8Sw2Usmi6hOscUp5rcucAN3X60Xh3wf4k1e1ju7ua105GHMsUuWk/wCQH5flnNUnUrFo
+NVms5MB7VzGeX+JTg4+1QemVwRkYxRGJzWd+H3HtrJDBo+sytDdKOSOdz8MnoCexrRRg4IIIPTFZ
+aHRtutCiEkChWappJkLjJrkaEnJo8rCgjYxvW4MN8XrNYeNr3GR5ipJ09VFRWg3XnQlMnmTYg1dP
+G+z/APGbO8HSa25D81J/oRWXQztYXvm78j7PVYqw6hFGSJOXp1OKbpzKOZExipGIxXduGT4sjam3
+lmJzG+du4NULW84YBZCPfaiXtosykxt13xXFCEEDOR39aNDKUk/KceuelEVq/gu4JcRrICD13okr
+XBhE8x+IHDEdSOxNW55oHT4lXPqKjr+2je0kjHLlxjtRURZNiPYjfvXWbAIYCkbZuVeQnp1HvSU8
+yDIBG+2KIW055DxDp7W7AP5uCcA/Dj4v0zVnhdpZIWOxMZkP/Ni230IqrcMRo2tyTqCTDbyMBnbJ
+HKP/AJVaLGRBcXDkhkiYRqADsFGB/KosSyxQOg50JPuKqvH7CJrG2RsjDyN+gFS9veTT3W4+HPrV
+X48uObWljBwI4gMDsTRTbhvTbjXtfstHtZooprqURh5Gwq+5qa8ZeCLjgi6s7d9VttRSUkiSIcpV
+h1BXP61VrW4ltikttIYZo2DI69Qw70e9utS1/VrSPULlp5ZZVXJwOpAJrSN7/wAO3B6Wul/9qL5R
++JuU5bcMu6Reo92/lWo2GmS6hqBv78AJGSLe3JyFH8Te/f2+dNOHEkt+FtOtRcRmNY1AePBGw2pT
+U9RFoHSW9htI+txcu3KIl74z1JOwHr8qjUTkY/Fo0Vs7RW6bM46k98e9Y54+No6alYWtjbRw3MMZ
+82RVwFQ9Ax9Sd9/ep3WvFzQ7G3FhollcXap8IkJ8tSPXfcn3xVP408QdP4k4Yk0M6GLbmkEvmGfn
+JcHOTsPehqhpb2kjiTl53H72dqv/AADxs9hyaZquTaDaObmyY/Y+1Z000iAsRGF6DkJOPnSE1xME
+ODzKR2rNiPTqyxTQrLDIskbDKspyCKFZ14IX7XGgXFm0vP5MpIBOcA9PpQrNaXlmwBtXRkkGkQwz
+SqZyKSireMVh+I4TivAPitJ1JP8ApbY/ry1h+pW3OTgnHbevQPHsN3qOjwaDYcguNTnEILdAoBcn
+/wBtYMRzKFIww2atxmmGl6nPps3lOeaPO4qz2t5aXyghwD71WLy18zJC71FwzSW8uCSKrK+XFpyn
+nifJPXemp8xdgjZ+RqOtLucqG5+YEbUs2pyoCWJoHEqyn8sbkkdlNRV4uoKwPkuvtykVIwcQRK2G
+yKPNrEEzpySFGXpk/Cw9DRVT1qeW1lWQoyCUZx03HWo1LtpG3JrdvBySz1fxAtbDUeG7PUomVjI8
+8SukAxs45ts5GPXrXp+z4b4chAaHQ9MjI6FbVB/SkMeG/CzTdS1jiRNOtdPu5TduiGZISyRKrcxZ
+j6bV6l4a8FuDLSxWO7hvb+U7vJNM0ZLHr8KYx+taqkcUIVI41VewUYArsigSZGKuKoKeEHAMJDpo
+rhvX8VKf5tTW78DfDW+na5udDleV8Zb8ZMOnyatLyCu/pQQgDqKDNbPwJ8M7W6juU4e8xozkLLcy
+un1Utg/WlX8EfDsasurW+hm3u0bmUxXEiqp9lzj9K0gkAda7nK7VRVZeDdIeJwqzwzH8s6vzPGfU
+Bsr+lecPGPh/ifh/iBbLW9Sl1GxkzJY3LfCGGdwR0DDv8/evWb7HOdqpvjLwsvGHh9qGnxIDf26G
+4sX7iVRnl/5br9amFeQiOQ4yCPbrTeSLLl4zv1371DQX8yA+YCGHX2NBuJCshR0UgbdKiHdxd8jl
+dwfSm348xnm5Oai/ibe/bflRj0P96ZXHNBLyMpA96GtG8G9cS34rWDIWO9Qxuv8AqG6n+n1oVV/D
+S3a7490qNCwVZfNbHooJ/pQrnZjUejBjPSl12IG1Nhkt0pxGD1NSVcQ3F17HpeqcOalNJyRRaj5b
+tnAAdGWsIlYJql7CSMpcOvX0Y1vvG3DZ4q4Yn0qO4W3nLLJDKwyEZTnt7ZFYxxfwJq3DV9LNNfR6
+j+xWeeRFK8nM3IOvXfG9bjNRbJzHAxnGMUwurJJVbb608t5gFywJJ9KcZXBBGRVREabM1rP5E26n
+cGp0Q288PMNjTC/tkki5kwCB2613SblsCF2APT50AvNMj5TIuB3z3qNSMBvi7Hap65TbP9dqirhV
+QFsYOelNMW7wn1ltC4uspjPy28kgSTmOwB6H23r2Fpl+rwhieY8ucj+9fP1794mJBAxWhcEeJHF1
+vYCCDU3Fra/xgNk9lyRnHtRXtA3S8iuCppvd38ceDg7HtWD6B40W7Wwe9tQZlHxRBsBv9p7VcND4
+80ni+ynbTGkt7222ntJtnXPQjsw9xV1V8XX7U8o5XO5H8v70dNcsOflZ+VvQmszvG1Yc7RJzxk5y
+N+tIQX8sbjmkhBO551II+9T9GNfTUbaTHJMjfI04S4UjGay+zvp5CpJBP+kbCp2y1GRByFmPfc9K
+foxb2lDE7/ShDIomUZ2OxHzquG8nZQyinNpdyNgEYYGmmPH3jLoFpo3iVrulEGBBcmaEqNuST4wP
+pzY+lUu60i2EBImVpPbvWwf4z7Q23iBpeoRLgXenAE+pR2/owrCo7iUOAT9KrIvJJbyfDkYNSkFx
+Hd24jlxzr+U0QeVdDHMFIG5NMJk8p/gl+1X0aj4Eaf5vFNzeFdrW2IB92IA/QGhVn8BdNntOF59U
+uYyn46UeVnqyLtn5Ek/ahXLq/Wo0LlAbY0dAc9TSJOD70ojjY1iNU7iyM4yKqfF8IvNa1KyfcT8N
+3DKD3aN1dftirVDKp9Kz/wAbLq60pdO1ywmWN+SazfftIvp8s1uJWNluQq/NsRnrTtJkbBUjGKj7
+OSOeE2rN8QGVNNj51jNljzRnvW2U7JKrKQoyR1phM/lSK64z1NKQOsq8/MObrmkJjzty8h9M1BOW
+E63UIQMOalZdBkul+J8DtvVTE01pLzwyMuPQU4bi3UUQJkEjbJFMNSl1wzbwjnurxQvcLTC61OGO
+FdO08IkS+nf5moDUtbvr5irynHtSNoxiIbO9WRFnVWEBfm3A9abadJxE16l3pN7cWk8eyyxyFDj0
+yO3tSNpqSD4ZcYPWp231e0t7blgYA43FBYLbxG474Zt+eW/ttULYDrOhJP1BGftVguOK+MNV4cte
+I7LheG4sp8q01rO+Y5FPxKy9j36dMVlVzfSX1zsMhdzWm/4fePLPhniSTh7XJUj0bWCFLufggnH5
+XPoD+Un5HtRZVq4J8Q+GDpbza/FrMFzAc3Kxwq/kL/ER1K57gbbVpPDHFPAuuMo0jjbT5XbcQzv5
+T/8Apf8AtTTirwj0niC9N7a6oulXirmKSABiT/qGd1x2715q8SOAZ9D4gm0y4jWw1INlMHFtdL2a
+Mn8pP8JqZFe1raxkKjlaGZT3RhinSWLKdosZr532mu8UcPXLQ2uralp8sZwViuHT+Rqaj8XPEeKE
+xJxfqnKRjeXJ+53rWJrYv8cd9YC94ask5Wv44pXch90jJUAEe5B+1eaDKSMgkGlNV1HUNVvZL3Ur
+ye7uZDl5ZnLMfqaluDODeIeLLvydHsHkjU4kuH+GKP5sf5DJ9qvgghI4YkM2/vWreEXhhfcQSxaz
+r8Ulto6kMkbfC917DuF9+/b1GkcB+EPDvDix3mphdX1Jfi5pF/Yxn/Snf5tn5Cr9PP22A6Vi9Lhp
+NHFEkcEEaxRRqFRFGAoHQAUKTnkHPmhXOqTcnNBGNChWI0VjbvgVnX+IiMtwbZzKQDHeA/dWoUK6
+c+s1g0N2/mrMuzKas0YS/tQ7LguvN8qFCt1kwV2tJ/KU5XNO2cqSQBkj0oUKBheFmzvudyah51Oe
+tChViVyOFQfenCxKRvQoVUJSRgHrSTMQ2ATQoVYsOIZ3gBAJpve3Ek5BJ70KFIJXh/i/ibQZEl0r
+Wbu2KkEKJCV/9J2q73Pi3xPxRZ/5JqENhNPe8tsLqSLLICcbD60KFLCM+4gDw63cWUkjTpaSNApb
+qQpIrWPD/wAMeG+IuDLW/vWvYbqUNl4ZRjrtsQaFCs9XIs9XHhvwa4M04CS9hudVl5sg3EnKg/4r
+jP1zWhW8dvY2kdpZW8NtbxjCRRIFVR7AUKFc9tawSSViDTSRsnehQqKbyjLbUKFCs0j/2Q==
+
+Cheerio!
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1--
--- /dev/null
+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
+--=-=-=--
--- /dev/null
+User-agent: mu4e 1.1.0; emacs 27.0.50
+From: Skipio <skipio@roma.net>
+To: Hannibal <hanni@carthago.net>
+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-----
+--=-=-=--
--- /dev/null
+Return-path: <>
+Envelope-to: skipio@localhost
+Delivery-date: Fri, 11 May 2012 16:21:57 +0300
+Received: from localhost.roma.net([127.0.0.1] helo=borealis)
+ by borealis with esmtp (Exim 4.77)
+ id 1SSpnB-00038a-55
+ for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300
+From: Skipio <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! 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-----
+--=-=-=--
+
--- /dev/null
+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-----
+--=-=-=--
+
--- /dev/null
+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.
--- /dev/null
+## 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.
+
+include $(top_srcdir)/gtest.mk
+
+EXTRA_DIST= \
+ expected.hpp \
+ optional.hpp \
+ tabulate.hpp
--- /dev/null
+///
+// expected - An implementation of std::expected with extensions
+// Written in 2017 by Simon Brand (simonrbrand@gmail.com, @TartanLlama)
+//
+// Documentation available at http://tl.tartanllama.xyz/
+//
+// To the extent possible under law, the author(s) have dedicated all
+// copyright and related and neighboring rights to this software to the
+// public domain worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication
+// along with this software. If not, see
+// <http://creativecommons.org/publicdomain/zero/1.0/>.
+///
+
+#ifndef TL_EXPECTED_HPP
+#define TL_EXPECTED_HPP
+
+#define TL_EXPECTED_VERSION_MAJOR 1
+#define TL_EXPECTED_VERSION_MINOR 0
+#define TL_EXPECTED_VERSION_PATCH 1
+
+#include <exception>
+#include <functional>
+#include <type_traits>
+#include <utility>
+
+#if defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define TL_EXPECTED_EXCEPTIONS_ENABLED
+#endif
+
+#if (defined(_MSC_VER) && _MSC_VER == 1900)
+#define TL_EXPECTED_MSVC2015
+#define TL_EXPECTED_MSVC2015_CONSTEXPR
+#else
+#define TL_EXPECTED_MSVC2015_CONSTEXPR constexpr
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \
+ !defined(__clang__))
+#define TL_EXPECTED_GCC49
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 && \
+ !defined(__clang__))
+#define TL_EXPECTED_GCC54
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 && \
+ !defined(__clang__))
+#define TL_EXPECTED_GCC55
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \
+ !defined(__clang__))
+// GCC < 5 doesn't support overloading on const&& for member functions
+
+#define TL_EXPECTED_NO_CONSTRR
+// GCC < 5 doesn't support some standard C++11 type traits
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ std::has_trivial_copy_constructor<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::has_trivial_copy_assign<T>
+
+// This one will be different for GCC 5.7 if it's ever supported
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \
+ std::is_trivially_destructible<T>
+
+// GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks std::vector
+// for non-copyable types
+#elif (defined(__GNUC__) && __GNUC__ < 8 && \
+ !defined(__clang__))
+#ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+#define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+namespace tl {
+ namespace detail {
+ template<class T>
+ struct is_trivially_copy_constructible : std::is_trivially_copy_constructible<T>{};
+#ifdef _GLIBCXX_VECTOR
+ template<class T, class A>
+ struct is_trivially_copy_constructible<std::vector<T,A>>
+ : std::false_type{};
+#endif
+ }
+}
+#endif
+
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ tl::detail::is_trivially_copy_constructible<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::is_trivially_copy_assignable<T>
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>
+#else
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ std::is_trivially_copy_constructible<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::is_trivially_copy_assignable<T>
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \
+ std::is_trivially_destructible<T>
+#endif
+
+#if __cplusplus > 201103L
+#define TL_EXPECTED_CXX14
+#endif
+
+#ifdef TL_EXPECTED_GCC49
+#define TL_EXPECTED_GCC49_CONSTEXPR
+#else
+#define TL_EXPECTED_GCC49_CONSTEXPR constexpr
+#endif
+
+#if (__cplusplus == 201103L || defined(TL_EXPECTED_MSVC2015) || \
+ defined(TL_EXPECTED_GCC49))
+#define TL_EXPECTED_11_CONSTEXPR
+#else
+#define TL_EXPECTED_11_CONSTEXPR constexpr
+#endif
+
+namespace tl {
+template <class T, class E> class expected;
+
+#ifndef TL_MONOSTATE_INPLACE_MUTEX
+#define TL_MONOSTATE_INPLACE_MUTEX
+class monostate {};
+
+struct in_place_t {
+ explicit in_place_t() = default;
+};
+static constexpr in_place_t in_place{};
+#endif
+
+template <class E> class unexpected {
+public:
+ static_assert(!std::is_same<E, void>::value, "E must not be void");
+
+ unexpected() = delete;
+ constexpr explicit unexpected(const E &e) : m_val(e) {}
+
+ constexpr explicit unexpected(E &&e) : m_val(std::move(e)) {}
+
+ constexpr const E &value() const & { return m_val; }
+ TL_EXPECTED_11_CONSTEXPR E &value() & { return m_val; }
+ TL_EXPECTED_11_CONSTEXPR E &&value() && { return std::move(m_val); }
+ constexpr const E &&value() const && { return std::move(m_val); }
+
+private:
+ E m_val;
+};
+
+template <class E>
+constexpr bool operator==(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() == rhs.value();
+}
+template <class E>
+constexpr bool operator!=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() != rhs.value();
+}
+template <class E>
+constexpr bool operator<(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() < rhs.value();
+}
+template <class E>
+constexpr bool operator<=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() <= rhs.value();
+}
+template <class E>
+constexpr bool operator>(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() > rhs.value();
+}
+template <class E>
+constexpr bool operator>=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() >= rhs.value();
+}
+
+template <class E>
+unexpected<typename std::decay<E>::type> make_unexpected(E &&e) {
+ return unexpected<typename std::decay<E>::type>(std::forward<E>(e));
+}
+
+struct unexpect_t {
+ unexpect_t() = default;
+};
+static constexpr unexpect_t unexpect{};
+
+namespace detail {
+template<typename E>
+[[noreturn]] TL_EXPECTED_11_CONSTEXPR void throw_exception(E &&e) {
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ throw std::forward<E>(e);
+#else
+ #ifdef _MSC_VER
+ __assume(0);
+ #else
+ __builtin_unreachable();
+ #endif
+#endif
+}
+
+#ifndef TL_TRAITS_MUTEX
+#define TL_TRAITS_MUTEX
+// C++14-style aliases for brevity
+template <class T> using remove_const_t = typename std::remove_const<T>::type;
+template <class T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template <class T> using decay_t = typename std::decay<T>::type;
+template <bool E, class T = void>
+using enable_if_t = typename std::enable_if<E, T>::type;
+template <bool B, class T, class F>
+using conditional_t = typename std::conditional<B, T, F>::type;
+
+// std::conjunction from C++17
+template <class...> struct conjunction : std::true_type {};
+template <class B> struct conjunction<B> : B {};
+template <class B, class... Bs>
+struct conjunction<B, Bs...>
+ : std::conditional<bool(B::value), conjunction<Bs...>, B>::type {};
+
+#if defined(_LIBCPP_VERSION) && __cplusplus == 201103L
+#define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+#endif
+
+// In C++11 mode, there's an issue in libc++'s std::mem_fn
+// which results in a hard-error when using it in a noexcept expression
+// in some cases. This is a check to workaround the common failing case.
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+template <class T> struct is_pointer_to_non_const_member_func : std::false_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...)> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...)&> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...) &&> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...) volatile> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...) volatile &> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...) volatile &&> : std::true_type {};
+
+template <class T> struct is_const_or_const_ref : std::false_type {};
+template <class T> struct is_const_or_const_ref<T const&> : std::true_type {};
+template <class T> struct is_const_or_const_ref<T const> : std::true_type {};
+#endif
+
+// std::invoke from C++17
+// https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround
+template <typename Fn, typename... Args,
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+ typename = enable_if_t<!(is_pointer_to_non_const_member_func<Fn>::value
+ && is_const_or_const_ref<Args...>::value)>,
+#endif
+ typename = enable_if_t<std::is_member_pointer<decay_t<Fn>>::value>,
+ int = 0>
+ constexpr auto invoke(Fn && f, Args && ... args) noexcept(
+ noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+ -> decltype(std::mem_fn(f)(std::forward<Args>(args)...)) {
+ return std::mem_fn(f)(std::forward<Args>(args)...);
+}
+
+template <typename Fn, typename... Args,
+ typename = enable_if_t<!std::is_member_pointer<decay_t<Fn>>::value>>
+ constexpr auto invoke(Fn && f, Args && ... args) noexcept(
+ noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+ -> decltype(std::forward<Fn>(f)(std::forward<Args>(args)...)) {
+ return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+// std::invoke_result from C++17
+template <class F, class, class... Us> struct invoke_result_impl;
+
+template <class F, class... Us>
+struct invoke_result_impl<
+ F, decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...), void()),
+ Us...> {
+ using type = decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...));
+};
+
+template <class F, class... Us>
+using invoke_result = invoke_result_impl<F, void, Us...>;
+
+template <class F, class... Us>
+using invoke_result_t = typename invoke_result<F, Us...>::type;
+
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+// TODO make a version which works with MSVC 2015
+template <class T, class U = T> struct is_swappable : std::true_type {};
+
+template <class T, class U = T> struct is_nothrow_swappable : std::true_type {};
+#else
+// https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept
+namespace swap_adl_tests {
+ // if swap ADL finds this then it would call std::swap otherwise (same
+ // signature)
+ struct tag {};
+
+ template <class T> tag swap(T&, T&);
+ template <class T, std::size_t N> tag swap(T(&a)[N], T(&b)[N]);
+
+ // helper functions to test if an unqualified swap is possible, and if it
+ // becomes std::swap
+ template <class, class> std::false_type can_swap(...) noexcept(false);
+ template <class T, class U,
+ class = decltype(swap(std::declval<T&>(), std::declval<U&>()))>
+ std::true_type can_swap(int) noexcept(noexcept(swap(std::declval<T&>(),
+ std::declval<U&>())));
+
+ template <class, class> std::false_type uses_std(...);
+ template <class T, class U>
+ std::is_same<decltype(swap(std::declval<T&>(), std::declval<U&>())), tag>
+ uses_std(int);
+
+ template <class T>
+ struct is_std_swap_noexcept
+ : std::integral_constant<bool,
+ std::is_nothrow_move_constructible<T>::value&&
+ std::is_nothrow_move_assignable<T>::value> {};
+
+ template <class T, std::size_t N>
+ struct is_std_swap_noexcept<T[N]> : is_std_swap_noexcept<T> {};
+
+ template <class T, class U>
+ struct is_adl_swap_noexcept
+ : std::integral_constant<bool, noexcept(can_swap<T, U>(0))> {};
+} // namespace swap_adl_tests
+
+template <class T, class U = T>
+struct is_swappable
+ : std::integral_constant<
+ bool,
+ decltype(detail::swap_adl_tests::can_swap<T, U>(0))::value &&
+ (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value ||
+ (std::is_move_assignable<T>::value &&
+ std::is_move_constructible<T>::value))> {};
+
+template <class T, std::size_t N>
+struct is_swappable<T[N], T[N]>
+ : std::integral_constant<
+ bool,
+ decltype(detail::swap_adl_tests::can_swap<T[N], T[N]>(0))::value &&
+ (!decltype(
+ detail::swap_adl_tests::uses_std<T[N], T[N]>(0))::value ||
+ is_swappable<T, T>::value)> {};
+
+template <class T, class U = T>
+struct is_nothrow_swappable
+ : std::integral_constant<
+ bool,
+ is_swappable<T, U>::value &&
+ ((decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value
+ && detail::swap_adl_tests::is_std_swap_noexcept<T>::value) ||
+ (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
+ detail::swap_adl_tests::is_adl_swap_noexcept<T,
+ U>::value))> {
+};
+#endif
+#endif
+
+// Trait for checking if a type is a tl::expected
+template <class T> struct is_expected_impl : std::false_type {};
+template <class T, class E>
+struct is_expected_impl<expected<T, E>> : std::true_type {};
+template <class T> using is_expected = is_expected_impl<decay_t<T>>;
+
+template <class T, class E, class U>
+using expected_enable_forward_value = detail::enable_if_t<
+ std::is_constructible<T, U &&>::value &&
+ !std::is_same<detail::decay_t<U>, in_place_t>::value &&
+ !std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+ !std::is_same<unexpected<E>, detail::decay_t<U>>::value>;
+
+template <class T, class E, class U, class G, class UR, class GR>
+using expected_enable_from_other = detail::enable_if_t<
+ std::is_constructible<T, UR>::value &&
+ std::is_constructible<E, GR>::value &&
+ !std::is_constructible<T, expected<U, G> &>::value &&
+ !std::is_constructible<T, expected<U, G> &&>::value &&
+ !std::is_constructible<T, const expected<U, G> &>::value &&
+ !std::is_constructible<T, const expected<U, G> &&>::value &&
+ !std::is_convertible<expected<U, G> &, T>::value &&
+ !std::is_convertible<expected<U, G> &&, T>::value &&
+ !std::is_convertible<const expected<U, G> &, T>::value &&
+ !std::is_convertible<const expected<U, G> &&, T>::value>;
+
+template <class T, class U>
+using is_void_or = conditional_t<std::is_void<T>::value, std::true_type, U>;
+
+template <class T>
+using is_copy_constructible_or_void =
+ is_void_or<T, std::is_copy_constructible<T>>;
+
+template <class T>
+using is_move_constructible_or_void =
+ is_void_or<T, std::is_move_constructible<T>>;
+
+template <class T>
+using is_copy_assignable_or_void =
+ is_void_or<T, std::is_copy_assignable<T>>;
+
+
+template <class T>
+using is_move_assignable_or_void =
+ is_void_or<T, std::is_move_assignable<T>>;
+
+
+} // namespace detail
+
+namespace detail {
+struct no_init_t {};
+static constexpr no_init_t no_init{};
+
+// Implements the storage of the values, and ensures that the destructor is
+// trivial if it can be.
+//
+// This specialization is for where neither `T` or `E` is trivially
+// destructible, so the destructors must be called on destruction of the
+// `expected`
+template <class T, class E, bool = std::is_trivially_destructible<T>::value,
+ bool = std::is_trivially_destructible<E>::value>
+struct expected_storage_base {
+ constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected_storage_base(in_place_t, Args &&... args)
+ : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+ Args &&... args)
+ : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&... args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() {
+ if (m_has_val) {
+ m_val.~T();
+ } else {
+ m_unexpect.~unexpected<E>();
+ }
+ }
+ union {
+ T m_val;
+ unexpected<E> m_unexpect;
+ char m_no_init;
+ };
+ bool m_has_val;
+};
+
+// This specialization is for when both `T` and `E` are trivially-destructible,
+// so the destructor of the `expected` can be trivial.
+template <class T, class E> struct expected_storage_base<T, E, true, true> {
+ constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected_storage_base(in_place_t, Args &&... args)
+ : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+ Args &&... args)
+ : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&... args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() = default;
+ union {
+ T m_val;
+ unexpected<E> m_unexpect;
+ char m_no_init;
+ };
+ bool m_has_val;
+};
+
+// T is trivial, E is not.
+template <class T, class E> struct expected_storage_base<T, E, true, false> {
+ constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+ TL_EXPECTED_MSVC2015_CONSTEXPR expected_storage_base(no_init_t)
+ : m_no_init(), m_has_val(false) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected_storage_base(in_place_t, Args &&... args)
+ : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+ Args &&... args)
+ : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&... args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() {
+ if (!m_has_val) {
+ m_unexpect.~unexpected<E>();
+ }
+ }
+
+ union {
+ T m_val;
+ unexpected<E> m_unexpect;
+ char m_no_init;
+ };
+ bool m_has_val;
+};
+
+// E is trivial, T is not.
+template <class T, class E> struct expected_storage_base<T, E, false, true> {
+ constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected_storage_base(in_place_t, Args &&... args)
+ : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+ Args &&... args)
+ : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&... args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() {
+ if (m_has_val) {
+ m_val.~T();
+ }
+ }
+ union {
+ T m_val;
+ unexpected<E> m_unexpect;
+ char m_no_init;
+ };
+ bool m_has_val;
+};
+
+// `T` is `void`, `E` is trivially-destructible
+template <class E> struct expected_storage_base<void, E, false, true> {
+ TL_EXPECTED_MSVC2015_CONSTEXPR expected_storage_base() : m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_val(), m_has_val(false) {}
+
+ constexpr expected_storage_base(in_place_t) : m_has_val(true) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&... args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() = default;
+ struct dummy {};
+ union {
+ unexpected<E> m_unexpect;
+ dummy m_val;
+ };
+ bool m_has_val;
+};
+
+// `T` is `void`, `E` is not trivially-destructible
+template <class E> struct expected_storage_base<void, E, false, false> {
+ constexpr expected_storage_base() : m_dummy(), m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_dummy(), m_has_val(false) {}
+
+ constexpr expected_storage_base(in_place_t) : m_dummy(), m_has_val(true) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&... args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() {
+ if (!m_has_val) {
+ m_unexpect.~unexpected<E>();
+ }
+ }
+
+ union {
+ unexpected<E> m_unexpect;
+ char m_dummy;
+ };
+ bool m_has_val;
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class T, class E>
+struct expected_operations_base : expected_storage_base<T, E> {
+ using expected_storage_base<T, E>::expected_storage_base;
+
+ template <class... Args> void construct(Args &&... args) noexcept {
+ new (std::addressof(this->m_val)) T(std::forward<Args>(args)...);
+ this->m_has_val = true;
+ }
+
+ template <class Rhs> void construct_with(Rhs &&rhs) noexcept {
+ new (std::addressof(this->m_val)) T(std::forward<Rhs>(rhs).get());
+ this->m_has_val = true;
+ }
+
+ template <class... Args> void construct_error(Args &&... args) noexcept {
+ new (std::addressof(this->m_unexpect))
+ unexpected<E>(std::forward<Args>(args)...);
+ this->m_has_val = false;
+ }
+
+ #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+
+ // These assign overloads ensure that the most efficient assignment
+ // implementation is used while maintaining the strong exception guarantee.
+ // The problematic case is where rhs has a value, but *this does not.
+ //
+ // This overload handles the case where we can just copy-construct `T`
+ // directly into place without throwing.
+ template <class U = T,
+ detail::enable_if_t<std::is_nothrow_copy_constructible<U>::value>
+ * = nullptr>
+ void assign(const expected_operations_base &rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct(rhs.get());
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ // This overload handles the case where we can attempt to create a copy of
+ // `T`, then no-throw move it into place if the copy was successful.
+ template <class U = T,
+ detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&
+ std::is_nothrow_move_constructible<U>::value>
+ * = nullptr>
+ void assign(const expected_operations_base &rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ T tmp = rhs.get();
+ geterr().~unexpected<E>();
+ construct(std::move(tmp));
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ // This overload is the worst-case, where we have to move-construct the
+ // unexpected value into temporary storage, then try to copy the T into place.
+ // If the construction succeeds, then everything is fine, but if it throws,
+ // then we move the old unexpected value back into place before rethrowing the
+ // exception.
+ template <class U = T,
+ detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&
+ !std::is_nothrow_move_constructible<U>::value>
+ * = nullptr>
+ void assign(const expected_operations_base &rhs) {
+ if (!this->m_has_val && rhs.m_has_val) {
+ auto tmp = std::move(geterr());
+ geterr().~unexpected<E>();
+
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ construct(rhs.get());
+ } catch (...) {
+ geterr() = std::move(tmp);
+ throw;
+ }
+#else
+ construct(rhs.get());
+#endif
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ // These overloads do the same as above, but for rvalues
+ template <class U = T,
+ detail::enable_if_t<std::is_nothrow_move_constructible<U>::value>
+ * = nullptr>
+ void assign(expected_operations_base &&rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct(std::move(rhs).get());
+ } else {
+ assign_common(std::move(rhs));
+ }
+ }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_nothrow_move_constructible<U>::value>
+ * = nullptr>
+ void assign(expected_operations_base &&rhs) {
+ if (!this->m_has_val && rhs.m_has_val) {
+ auto tmp = std::move(geterr());
+ geterr().~unexpected<E>();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ construct(std::move(rhs).get());
+ } catch (...) {
+ geterr() = std::move(tmp);
+ throw;
+ }
+#else
+ construct(std::move(rhs).get());
+#endif
+ } else {
+ assign_common(std::move(rhs));
+ }
+ }
+
+ #else
+
+ // If exceptions are disabled then we can just copy-construct
+ void assign(const expected_operations_base &rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct(rhs.get());
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ void assign(expected_operations_base &&rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct(std::move(rhs).get());
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ #endif
+
+ // The common part of move/copy assigning
+ template <class Rhs> void assign_common(Rhs &&rhs) {
+ if (this->m_has_val) {
+ if (rhs.m_has_val) {
+ get() = std::forward<Rhs>(rhs).get();
+ } else {
+ destroy_val();
+ construct_error(std::forward<Rhs>(rhs).geterr());
+ }
+ } else {
+ if (!rhs.m_has_val) {
+ geterr() = std::forward<Rhs>(rhs).geterr();
+ }
+ }
+ }
+
+ bool has_value() const { return this->m_has_val; }
+
+ TL_EXPECTED_11_CONSTEXPR T &get() & { return this->m_val; }
+ constexpr const T &get() const & { return this->m_val; }
+ TL_EXPECTED_11_CONSTEXPR T &&get() && { return std::move(this->m_val); }
+#ifndef TL_EXPECTED_NO_CONSTRR
+ constexpr const T &&get() const && { return std::move(this->m_val); }
+#endif
+
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &geterr() & {
+ return this->m_unexpect;
+ }
+ constexpr const unexpected<E> &geterr() const & { return this->m_unexpect; }
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &&geterr() && {
+ return std::move(this->m_unexpect);
+ }
+#ifndef TL_EXPECTED_NO_CONSTRR
+ constexpr const unexpected<E> &&geterr() const && {
+ return std::move(this->m_unexpect);
+ }
+#endif
+
+ TL_EXPECTED_11_CONSTEXPR void destroy_val() {
+ get().~T();
+ }
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class E>
+struct expected_operations_base<void, E> : expected_storage_base<void, E> {
+ using expected_storage_base<void, E>::expected_storage_base;
+
+ template <class... Args> void construct() noexcept { this->m_has_val = true; }
+
+ // This function doesn't use its argument, but needs it so that code in
+ // levels above this can work independently of whether T is void
+ template <class Rhs> void construct_with(Rhs &&) noexcept {
+ this->m_has_val = true;
+ }
+
+ template <class... Args> void construct_error(Args &&... args) noexcept {
+ new (std::addressof(this->m_unexpect))
+ unexpected<E>(std::forward<Args>(args)...);
+ this->m_has_val = false;
+ }
+
+ template <class Rhs> void assign(Rhs &&rhs) noexcept {
+ if (!this->m_has_val) {
+ if (rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct();
+ } else {
+ geterr() = std::forward<Rhs>(rhs).geterr();
+ }
+ } else {
+ if (!rhs.m_has_val) {
+ construct_error(std::forward<Rhs>(rhs).geterr());
+ }
+ }
+ }
+
+ bool has_value() const { return this->m_has_val; }
+
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &geterr() & {
+ return this->m_unexpect;
+ }
+ constexpr const unexpected<E> &geterr() const & { return this->m_unexpect; }
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &&geterr() && {
+ return std::move(this->m_unexpect);
+ }
+#ifndef TL_EXPECTED_NO_CONSTRR
+ constexpr const unexpected<E> &&geterr() const && {
+ return std::move(this->m_unexpect);
+ }
+#endif
+
+ TL_EXPECTED_11_CONSTEXPR void destroy_val() {
+ //no-op
+ }
+};
+
+// This class manages conditionally having a trivial copy constructor
+// This specialization is for when T and E are trivially copy constructible
+template <class T, class E,
+ bool = is_void_or<T, TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)>::
+ value &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value>
+struct expected_copy_base : expected_operations_base<T, E> {
+ using expected_operations_base<T, E>::expected_operations_base;
+};
+
+// This specialization is for when T or E are not trivially copy constructible
+template <class T, class E>
+struct expected_copy_base<T, E, false> : expected_operations_base<T, E> {
+ using expected_operations_base<T, E>::expected_operations_base;
+
+ expected_copy_base() = default;
+ expected_copy_base(const expected_copy_base &rhs)
+ : expected_operations_base<T, E>(no_init) {
+ if (rhs.has_value()) {
+ this->construct_with(rhs);
+ } else {
+ this->construct_error(rhs.geterr());
+ }
+ }
+
+ expected_copy_base(expected_copy_base &&rhs) = default;
+ expected_copy_base &operator=(const expected_copy_base &rhs) = default;
+ expected_copy_base &operator=(expected_copy_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move constructor
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_constructible. We
+// have to make do with a non-trivial move constructor even if T is trivially
+// move constructible
+#ifndef TL_EXPECTED_GCC49
+template <class T, class E,
+ bool = is_void_or<T, std::is_trivially_move_constructible<T>>::value
+ &&std::is_trivially_move_constructible<E>::value>
+struct expected_move_base : expected_copy_base<T, E> {
+ using expected_copy_base<T, E>::expected_copy_base;
+};
+#else
+template <class T, class E, bool = false> struct expected_move_base;
+#endif
+template <class T, class E>
+struct expected_move_base<T, E, false> : expected_copy_base<T, E> {
+ using expected_copy_base<T, E>::expected_copy_base;
+
+ expected_move_base() = default;
+ expected_move_base(const expected_move_base &rhs) = default;
+
+ expected_move_base(expected_move_base &&rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value)
+ : expected_copy_base<T, E>(no_init) {
+ if (rhs.has_value()) {
+ this->construct_with(std::move(rhs));
+ } else {
+ this->construct_error(std::move(rhs.geterr()));
+ }
+ }
+ expected_move_base &operator=(const expected_move_base &rhs) = default;
+ expected_move_base &operator=(expected_move_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial copy assignment operator
+template <class T, class E,
+ bool = is_void_or<
+ T, conjunction<TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T),
+ TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T),
+ TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T)>>::value
+ &&TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(E)::value
+ &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value
+ &&TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(E)::value>
+struct expected_copy_assign_base : expected_move_base<T, E> {
+ using expected_move_base<T, E>::expected_move_base;
+};
+
+template <class T, class E>
+struct expected_copy_assign_base<T, E, false> : expected_move_base<T, E> {
+ using expected_move_base<T, E>::expected_move_base;
+
+ expected_copy_assign_base() = default;
+ expected_copy_assign_base(const expected_copy_assign_base &rhs) = default;
+
+ expected_copy_assign_base(expected_copy_assign_base &&rhs) = default;
+ expected_copy_assign_base &operator=(const expected_copy_assign_base &rhs) {
+ this->assign(rhs);
+ return *this;
+ }
+ expected_copy_assign_base &
+ operator=(expected_copy_assign_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move assignment operator
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_assignable. We have
+// to make do with a non-trivial move assignment operator even if T is trivially
+// move assignable
+#ifndef TL_EXPECTED_GCC49
+template <class T, class E,
+ bool =
+ is_void_or<T, conjunction<std::is_trivially_destructible<T>,
+ std::is_trivially_move_constructible<T>,
+ std::is_trivially_move_assignable<T>>>::
+ value &&std::is_trivially_destructible<E>::value
+ &&std::is_trivially_move_constructible<E>::value
+ &&std::is_trivially_move_assignable<E>::value>
+struct expected_move_assign_base : expected_copy_assign_base<T, E> {
+ using expected_copy_assign_base<T, E>::expected_copy_assign_base;
+};
+#else
+template <class T, class E, bool = false> struct expected_move_assign_base;
+#endif
+
+template <class T, class E>
+struct expected_move_assign_base<T, E, false>
+ : expected_copy_assign_base<T, E> {
+ using expected_copy_assign_base<T, E>::expected_copy_assign_base;
+
+ expected_move_assign_base() = default;
+ expected_move_assign_base(const expected_move_assign_base &rhs) = default;
+
+ expected_move_assign_base(expected_move_assign_base &&rhs) = default;
+
+ expected_move_assign_base &
+ operator=(const expected_move_assign_base &rhs) = default;
+
+ expected_move_assign_base &
+ operator=(expected_move_assign_base &&rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value
+ &&std::is_nothrow_move_assignable<T>::value) {
+ this->assign(std::move(rhs));
+ return *this;
+ }
+};
+
+// expected_delete_ctor_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible
+template <class T, class E,
+ bool EnableCopy = (is_copy_constructible_or_void<T>::value &&
+ std::is_copy_constructible<E>::value),
+ bool EnableMove = (is_move_constructible_or_void<T>::value &&
+ std::is_move_constructible<E>::value)>
+struct expected_delete_ctor_base {
+ expected_delete_ctor_base() = default;
+ expected_delete_ctor_base(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default;
+ expected_delete_ctor_base &
+ operator=(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base &
+ operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, true, false> {
+ expected_delete_ctor_base() = default;
+ expected_delete_ctor_base(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete;
+ expected_delete_ctor_base &
+ operator=(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base &
+ operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, false, true> {
+ expected_delete_ctor_base() = default;
+ expected_delete_ctor_base(const expected_delete_ctor_base &) = delete;
+ expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default;
+ expected_delete_ctor_base &
+ operator=(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base &
+ operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, false, false> {
+ expected_delete_ctor_base() = default;
+ expected_delete_ctor_base(const expected_delete_ctor_base &) = delete;
+ expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete;
+ expected_delete_ctor_base &
+ operator=(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base &
+ operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+// expected_delete_assign_base will conditionally delete copy and move
+// constructors depending on whether T and E are copy/move constructible +
+// assignable
+template <class T, class E,
+ bool EnableCopy = (is_copy_constructible_or_void<T>::value &&
+ std::is_copy_constructible<E>::value &&
+ is_copy_assignable_or_void<T>::value &&
+ std::is_copy_assignable<E>::value),
+ bool EnableMove = (is_move_constructible_or_void<T>::value &&
+ std::is_move_constructible<E>::value &&
+ is_move_assignable_or_void<T>::value &&
+ std::is_move_assignable<E>::value)>
+struct expected_delete_assign_base {
+ expected_delete_assign_base() = default;
+ expected_delete_assign_base(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+ default;
+ expected_delete_assign_base &
+ operator=(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base &
+ operator=(expected_delete_assign_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, true, false> {
+ expected_delete_assign_base() = default;
+ expected_delete_assign_base(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+ default;
+ expected_delete_assign_base &
+ operator=(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base &
+ operator=(expected_delete_assign_base &&) noexcept = delete;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, false, true> {
+ expected_delete_assign_base() = default;
+ expected_delete_assign_base(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+ default;
+ expected_delete_assign_base &
+ operator=(const expected_delete_assign_base &) = delete;
+ expected_delete_assign_base &
+ operator=(expected_delete_assign_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, false, false> {
+ expected_delete_assign_base() = default;
+ expected_delete_assign_base(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+ default;
+ expected_delete_assign_base &
+ operator=(const expected_delete_assign_base &) = delete;
+ expected_delete_assign_base &
+ operator=(expected_delete_assign_base &&) noexcept = delete;
+};
+
+// This is needed to be able to construct the expected_default_ctor_base which
+// follows, while still conditionally deleting the default constructor.
+struct default_constructor_tag {
+ explicit constexpr default_constructor_tag() = default;
+};
+
+// expected_default_ctor_base will ensure that expected has a deleted default
+// consturctor if T is not default constructible.
+// This specialization is for when T is default constructible
+template <class T, class E,
+ bool Enable =
+ std::is_default_constructible<T>::value || std::is_void<T>::value>
+struct expected_default_ctor_base {
+ constexpr expected_default_ctor_base() noexcept = default;
+ constexpr expected_default_ctor_base(
+ expected_default_ctor_base const &) noexcept = default;
+ constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept =
+ default;
+ expected_default_ctor_base &
+ operator=(expected_default_ctor_base const &) noexcept = default;
+ expected_default_ctor_base &
+ operator=(expected_default_ctor_base &&) noexcept = default;
+
+ constexpr explicit expected_default_ctor_base(default_constructor_tag) {}
+};
+
+// This specialization is for when T is not default constructible
+template <class T, class E> struct expected_default_ctor_base<T, E, false> {
+ constexpr expected_default_ctor_base() noexcept = delete;
+ constexpr expected_default_ctor_base(
+ expected_default_ctor_base const &) noexcept = default;
+ constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept =
+ default;
+ expected_default_ctor_base &
+ operator=(expected_default_ctor_base const &) noexcept = default;
+ expected_default_ctor_base &
+ operator=(expected_default_ctor_base &&) noexcept = default;
+
+ constexpr explicit expected_default_ctor_base(default_constructor_tag) {}
+};
+} // namespace detail
+
+template <class E> class bad_expected_access : public std::exception {
+public:
+ explicit bad_expected_access(E e) : m_val(std::move(e)) {}
+
+ virtual const char *what() const noexcept override {
+ return "Bad expected access";
+ }
+
+ const E &error() const & { return m_val; }
+ E &error() & { return m_val; }
+ const E &&error() const && { return std::move(m_val); }
+ E &&error() && { return std::move(m_val); }
+
+private:
+ E m_val;
+};
+
+/// An `expected<T, E>` object is an object that contains the storage for
+/// another object and manages the lifetime of this contained object `T`.
+/// Alternatively it could contain the storage for another unexpected object
+/// `E`. The contained object may not be initialized after the expected object
+/// has been initialized, and may not be destroyed before the expected object
+/// has been destroyed. The initialization state of the contained object is
+/// tracked by the expected object.
+template <class T, class E>
+class expected : private detail::expected_move_assign_base<T, E>,
+ private detail::expected_delete_ctor_base<T, E>,
+ private detail::expected_delete_assign_base<T, E>,
+ private detail::expected_default_ctor_base<T, E> {
+ static_assert(!std::is_reference<T>::value, "T must not be a reference");
+ static_assert(!std::is_same<T, std::remove_cv<in_place_t>::type>::value,
+ "T must not be in_place_t");
+ static_assert(!std::is_same<T, std::remove_cv<unexpect_t>::type>::value,
+ "T must not be unexpect_t");
+ static_assert(!std::is_same<T, typename std::remove_cv<unexpected<E>>::type>::value,
+ "T must not be unexpected<E>");
+ static_assert(!std::is_reference<E>::value, "E must not be a reference");
+
+ T *valptr() { return std::addressof(this->m_val); }
+ const T *valptr() const { return std::addressof(this->m_val); }
+ unexpected<E> *errptr() { return std::addressof(this->m_unexpect); }
+ const unexpected<E> *errptr() const { return std::addressof(this->m_unexpect); }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &val() {
+ return this->m_val;
+ }
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &err() { return this->m_unexpect; }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ constexpr const U &val() const {
+ return this->m_val;
+ }
+ constexpr const unexpected<E> &err() const { return this->m_unexpect; }
+
+ using impl_base = detail::expected_move_assign_base<T, E>;
+ using ctor_base = detail::expected_default_ctor_base<T, E>;
+
+public:
+ typedef T value_type;
+ typedef E error_type;
+ typedef unexpected<E> unexpected_type;
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) & {
+ return and_then_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && {
+ return and_then_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto and_then(F &&f) const & {
+ return and_then_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F> constexpr auto and_then(F &&f) const && {
+ return and_then_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR auto
+ and_then(F &&f) & -> decltype(and_then_impl(std::declval<expected&>(), std::forward<F>(f))) {
+ return and_then_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && -> decltype(
+ and_then_impl(std::declval<expected&&>(), std::forward<F>(f))) {
+ return and_then_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr auto and_then(F &&f) const & -> decltype(
+ and_then_impl(std::declval<expected const&>(), std::forward<F>(f))) {
+ return and_then_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr auto and_then(F &&f) const && -> decltype(
+ and_then_impl(std::declval<expected const&&>(), std::forward<F>(f))) {
+ return and_then_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto map(F &&f) & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto map(F &&f) && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto map(F &&f) const & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F> constexpr auto map(F &&f) const && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(
+ expected_map_impl(std::declval<expected &>(), std::declval<F &&>()))
+ map(F &&f) & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(
+ expected_map_impl(std::declval<expected>(), std::declval<F &&>()))
+ map(F &&f) && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr decltype(expected_map_impl(std::declval<const expected &>(),
+ std::declval<F &&>()))
+ map(F &&f) const & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr decltype(expected_map_impl(std::declval<const expected &&>(),
+ std::declval<F &&>()))
+ map(F &&f) const && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto transform(F &&f) const & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F> constexpr auto transform(F &&f) const && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(
+ expected_map_impl(std::declval<expected &>(), std::declval<F &&>()))
+ transform(F &&f) & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(
+ expected_map_impl(std::declval<expected>(), std::declval<F &&>()))
+ transform(F &&f) && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr decltype(expected_map_impl(std::declval<const expected &>(),
+ std::declval<F &&>()))
+ transform(F &&f) const & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr decltype(expected_map_impl(std::declval<const expected &&>(),
+ std::declval<F &&>()))
+ transform(F &&f) const && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto map_error(F &&f) const & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F> constexpr auto map_error(F &&f) const && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval<expected &>(),
+ std::declval<F &&>()))
+ map_error(F &&f) & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval<expected &&>(),
+ std::declval<F &&>()))
+ map_error(F &&f) && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr decltype(map_error_impl(std::declval<const expected &>(),
+ std::declval<F &&>()))
+ map_error(F &&f) const & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr decltype(map_error_impl(std::declval<const expected &&>(),
+ std::declval<F &&>()))
+ map_error(F &&f) const && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+ template <class F> expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) & {
+ return or_else_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) && {
+ return or_else_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> expected constexpr or_else(F &&f) const & {
+ return or_else_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F> expected constexpr or_else(F &&f) const && {
+ return or_else_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+ constexpr expected() = default;
+ constexpr expected(const expected &rhs) = default;
+ constexpr expected(expected &&rhs) = default;
+ expected &operator=(const expected &rhs) = default;
+ expected &operator=(expected &&rhs) = default;
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected(in_place_t, Args &&... args)
+ : impl_base(in_place, std::forward<Args>(args)...),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected(in_place_t, std::initializer_list<U> il, Args &&... args)
+ : impl_base(in_place, il, std::forward<Args>(args)...),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class G = E,
+ detail::enable_if_t<std::is_constructible<E, const G &>::value> * =
+ nullptr,
+ detail::enable_if_t<!std::is_convertible<const G &, E>::value> * =
+ nullptr>
+ explicit constexpr expected(const unexpected<G> &e)
+ : impl_base(unexpect, e.value()),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <
+ class G = E,
+ detail::enable_if_t<std::is_constructible<E, const G &>::value> * =
+ nullptr,
+ detail::enable_if_t<std::is_convertible<const G &, E>::value> * = nullptr>
+ constexpr expected(unexpected<G> const &e)
+ : impl_base(unexpect, e.value()),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <
+ class G = E,
+ detail::enable_if_t<std::is_constructible<E, G &&>::value> * = nullptr,
+ detail::enable_if_t<!std::is_convertible<G &&, E>::value> * = nullptr>
+ explicit constexpr expected(unexpected<G> &&e) noexcept(
+ std::is_nothrow_constructible<E, G &&>::value)
+ : impl_base(unexpect, std::move(e.value())),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <
+ class G = E,
+ detail::enable_if_t<std::is_constructible<E, G &&>::value> * = nullptr,
+ detail::enable_if_t<std::is_convertible<G &&, E>::value> * = nullptr>
+ constexpr expected(unexpected<G> &&e) noexcept(
+ std::is_nothrow_constructible<E, G &&>::value)
+ : impl_base(unexpect, std::move(e.value())),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected(unexpect_t, Args &&... args)
+ : impl_base(unexpect, std::forward<Args>(args)...),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected(unexpect_t, std::initializer_list<U> il,
+ Args &&... args)
+ : impl_base(unexpect, il, std::forward<Args>(args)...),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class U, class G,
+ detail::enable_if_t<!(std::is_convertible<U const &, T>::value &&
+ std::is_convertible<G const &, E>::value)> * =
+ nullptr,
+ detail::expected_enable_from_other<T, E, U, G, const U &, const G &>
+ * = nullptr>
+ explicit TL_EXPECTED_11_CONSTEXPR expected(const expected<U, G> &rhs)
+ : ctor_base(detail::default_constructor_tag{}) {
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ } else {
+ this->construct_error(rhs.error());
+ }
+ }
+
+ template <class U, class G,
+ detail::enable_if_t<(std::is_convertible<U const &, T>::value &&
+ std::is_convertible<G const &, E>::value)> * =
+ nullptr,
+ detail::expected_enable_from_other<T, E, U, G, const U &, const G &>
+ * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR expected(const expected<U, G> &rhs)
+ : ctor_base(detail::default_constructor_tag{}) {
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ } else {
+ this->construct_error(rhs.error());
+ }
+ }
+
+ template <
+ class U, class G,
+ detail::enable_if_t<!(std::is_convertible<U &&, T>::value &&
+ std::is_convertible<G &&, E>::value)> * = nullptr,
+ detail::expected_enable_from_other<T, E, U, G, U &&, G &&> * = nullptr>
+ explicit TL_EXPECTED_11_CONSTEXPR expected(expected<U, G> &&rhs)
+ : ctor_base(detail::default_constructor_tag{}) {
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ } else {
+ this->construct_error(std::move(rhs.error()));
+ }
+ }
+
+ template <
+ class U, class G,
+ detail::enable_if_t<(std::is_convertible<U &&, T>::value &&
+ std::is_convertible<G &&, E>::value)> * = nullptr,
+ detail::expected_enable_from_other<T, E, U, G, U &&, G &&> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR expected(expected<U, G> &&rhs)
+ : ctor_base(detail::default_constructor_tag{}) {
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ } else {
+ this->construct_error(std::move(rhs.error()));
+ }
+ }
+
+ template <
+ class U = T,
+ detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr,
+ detail::expected_enable_forward_value<T, E, U> * = nullptr>
+ explicit TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v)
+ : expected(in_place, std::forward<U>(v)) {}
+
+ template <
+ class U = T,
+ detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr,
+ detail::expected_enable_forward_value<T, E, U> * = nullptr>
+ TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v)
+ : expected(in_place, std::forward<U>(v)) {}
+
+ template <
+ class U = T, class G = T,
+ detail::enable_if_t<std::is_nothrow_constructible<T, U &&>::value> * =
+ nullptr,
+ detail::enable_if_t<!std::is_void<G>::value> * = nullptr,
+ detail::enable_if_t<
+ (!std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+ !detail::conjunction<std::is_scalar<T>,
+ std::is_same<T, detail::decay_t<U>>>::value &&
+ std::is_constructible<T, U>::value &&
+ std::is_assignable<G &, U>::value &&
+ std::is_nothrow_move_constructible<E>::value)> * = nullptr>
+ expected &operator=(U &&v) {
+ if (has_value()) {
+ val() = std::forward<U>(v);
+ } else {
+ err().~unexpected<E>();
+ ::new (valptr()) T(std::forward<U>(v));
+ this->m_has_val = true;
+ }
+
+ return *this;
+ }
+
+ template <
+ class U = T, class G = T,
+ detail::enable_if_t<!std::is_nothrow_constructible<T, U &&>::value> * =
+ nullptr,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr,
+ detail::enable_if_t<
+ (!std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+ !detail::conjunction<std::is_scalar<T>,
+ std::is_same<T, detail::decay_t<U>>>::value &&
+ std::is_constructible<T, U>::value &&
+ std::is_assignable<G &, U>::value &&
+ std::is_nothrow_move_constructible<E>::value)> * = nullptr>
+ expected &operator=(U &&v) {
+ if (has_value()) {
+ val() = std::forward<U>(v);
+ } else {
+ auto tmp = std::move(err());
+ err().~unexpected<E>();
+
+ #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (valptr()) T(std::forward<U>(v));
+ this->m_has_val = true;
+ } catch (...) {
+ err() = std::move(tmp);
+ throw;
+ }
+ #else
+ ::new (valptr()) T(std::forward<U>(v));
+ this->m_has_val = true;
+ #endif
+ }
+
+ return *this;
+ }
+
+ template <class G = E,
+ detail::enable_if_t<std::is_nothrow_copy_constructible<G>::value &&
+ std::is_assignable<G &, G>::value> * = nullptr>
+ expected &operator=(const unexpected<G> &rhs) {
+ if (!has_value()) {
+ err() = rhs;
+ } else {
+ this->destroy_val();
+ ::new (errptr()) unexpected<E>(rhs);
+ this->m_has_val = false;
+ }
+
+ return *this;
+ }
+
+ template <class G = E,
+ detail::enable_if_t<std::is_nothrow_move_constructible<G>::value &&
+ std::is_move_assignable<G>::value> * = nullptr>
+ expected &operator=(unexpected<G> &&rhs) noexcept {
+ if (!has_value()) {
+ err() = std::move(rhs);
+ } else {
+ this->destroy_val();
+ ::new (errptr()) unexpected<E>(std::move(rhs));
+ this->m_has_val = false;
+ }
+
+ return *this;
+ }
+
+ template <class... Args, detail::enable_if_t<std::is_nothrow_constructible<
+ T, Args &&...>::value> * = nullptr>
+ void emplace(Args &&... args) {
+ if (has_value()) {
+ val() = T(std::forward<Args>(args)...);
+ } else {
+ err().~unexpected<E>();
+ ::new (valptr()) T(std::forward<Args>(args)...);
+ this->m_has_val = true;
+ }
+ }
+
+ template <class... Args, detail::enable_if_t<!std::is_nothrow_constructible<
+ T, Args &&...>::value> * = nullptr>
+ void emplace(Args &&... args) {
+ if (has_value()) {
+ val() = T(std::forward<Args>(args)...);
+ } else {
+ auto tmp = std::move(err());
+ err().~unexpected<E>();
+
+ #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (valptr()) T(std::forward<Args>(args)...);
+ this->m_has_val = true;
+ } catch (...) {
+ err() = std::move(tmp);
+ throw;
+ }
+ #else
+ ::new (valptr()) T(std::forward<Args>(args)...);
+ this->m_has_val = true;
+ #endif
+ }
+ }
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_nothrow_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ void emplace(std::initializer_list<U> il, Args &&... args) {
+ if (has_value()) {
+ T t(il, std::forward<Args>(args)...);
+ val() = std::move(t);
+ } else {
+ err().~unexpected<E>();
+ ::new (valptr()) T(il, std::forward<Args>(args)...);
+ this->m_has_val = true;
+ }
+ }
+
+ template <class U, class... Args,
+ detail::enable_if_t<!std::is_nothrow_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ void emplace(std::initializer_list<U> il, Args &&... args) {
+ if (has_value()) {
+ T t(il, std::forward<Args>(args)...);
+ val() = std::move(t);
+ } else {
+ auto tmp = std::move(err());
+ err().~unexpected<E>();
+
+ #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (valptr()) T(il, std::forward<Args>(args)...);
+ this->m_has_val = true;
+ } catch (...) {
+ err() = std::move(tmp);
+ throw;
+ }
+ #else
+ ::new (valptr()) T(il, std::forward<Args>(args)...);
+ this->m_has_val = true;
+ #endif
+ }
+ }
+
+private:
+ using t_is_void = std::true_type;
+ using t_is_not_void = std::false_type;
+ using t_is_nothrow_move_constructible = std::true_type;
+ using move_constructing_t_can_throw = std::false_type;
+ using e_is_nothrow_move_constructible = std::true_type;
+ using move_constructing_e_can_throw = std::false_type;
+
+ void swap_where_both_have_value(expected &/*rhs*/ , t_is_void) noexcept {
+ // swapping void is a no-op
+ }
+
+ void swap_where_both_have_value(expected &rhs, t_is_not_void) {
+ using std::swap;
+ swap(val(), rhs.val());
+ }
+
+ void swap_where_only_one_has_value(expected &rhs, t_is_void) noexcept(
+ std::is_nothrow_move_constructible<E>::value) {
+ ::new (errptr()) unexpected_type(std::move(rhs.err()));
+ rhs.err().~unexpected_type();
+ std::swap(this->m_has_val, rhs.m_has_val);
+ }
+
+ void swap_where_only_one_has_value(expected &rhs, t_is_not_void) {
+ swap_where_only_one_has_value_and_t_is_not_void(
+ rhs, typename std::is_nothrow_move_constructible<T>::type{},
+ typename std::is_nothrow_move_constructible<E>::type{});
+ }
+
+ void swap_where_only_one_has_value_and_t_is_not_void(
+ expected &rhs, t_is_nothrow_move_constructible,
+ e_is_nothrow_move_constructible) noexcept {
+ auto temp = std::move(val());
+ val().~T();
+ ::new (errptr()) unexpected_type(std::move(rhs.err()));
+ rhs.err().~unexpected_type();
+ ::new (rhs.valptr()) T(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+ }
+
+ void swap_where_only_one_has_value_and_t_is_not_void(
+ expected &rhs, t_is_nothrow_move_constructible,
+ move_constructing_e_can_throw) {
+ auto temp = std::move(val());
+ val().~T();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (errptr()) unexpected_type(std::move(rhs.err()));
+ rhs.err().~unexpected_type();
+ ::new (rhs.valptr()) T(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+ } catch (...) {
+ val() = std::move(temp);
+ throw;
+ }
+#else
+ ::new (errptr()) unexpected_type(std::move(rhs.err()));
+ rhs.err().~unexpected_type();
+ ::new (rhs.valptr()) T(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+#endif
+ }
+
+ void swap_where_only_one_has_value_and_t_is_not_void(
+ expected &rhs, move_constructing_t_can_throw,
+ t_is_nothrow_move_constructible) {
+ auto temp = std::move(rhs.err());
+ rhs.err().~unexpected_type();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (rhs.valptr()) T(val());
+ val().~T();
+ ::new (errptr()) unexpected_type(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+ } catch (...) {
+ rhs.err() = std::move(temp);
+ throw;
+ }
+#else
+ ::new (rhs.valptr()) T(val());
+ val().~T();
+ ::new (errptr()) unexpected_type(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+#endif
+ }
+
+public:
+ template <class OT = T, class OE = E>
+ detail::enable_if_t<detail::is_swappable<OT>::value &&
+ detail::is_swappable<OE>::value &&
+ (std::is_nothrow_move_constructible<OT>::value ||
+ std::is_nothrow_move_constructible<OE>::value)>
+ swap(expected &rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value
+ &&detail::is_nothrow_swappable<T>::value
+ &&std::is_nothrow_move_constructible<E>::value
+ &&detail::is_nothrow_swappable<E>::value) {
+ if (has_value() && rhs.has_value()) {
+ swap_where_both_have_value(rhs, typename std::is_void<T>::type{});
+ } else if (!has_value() && rhs.has_value()) {
+ rhs.swap(*this);
+ } else if (has_value()) {
+ swap_where_only_one_has_value(rhs, typename std::is_void<T>::type{});
+ } else {
+ using std::swap;
+ swap(err(), rhs.err());
+ }
+ }
+
+ constexpr const T *operator->() const { return valptr(); }
+ TL_EXPECTED_11_CONSTEXPR T *operator->() { return valptr(); }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ constexpr const U &operator*() const & {
+ return val();
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &operator*() & {
+ return val();
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ constexpr const U &&operator*() const && {
+ return std::move(val());
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &&operator*() && {
+ return std::move(val());
+ }
+
+ constexpr bool has_value() const noexcept { return this->m_has_val; }
+ constexpr explicit operator bool() const noexcept { return this->m_has_val; }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR const U &value() const & {
+ if (!has_value())
+ detail::throw_exception(bad_expected_access<E>(err().value()));
+ return val();
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &value() & {
+ if (!has_value())
+ detail::throw_exception(bad_expected_access<E>(err().value()));
+ return val();
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR const U &&value() const && {
+ if (!has_value())
+ detail::throw_exception(bad_expected_access<E>(std::move(err()).value()));
+ return std::move(val());
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &&value() && {
+ if (!has_value())
+ detail::throw_exception(bad_expected_access<E>(std::move(err()).value()));
+ return std::move(val());
+ }
+
+ constexpr const E &error() const & { return err().value(); }
+ TL_EXPECTED_11_CONSTEXPR E &error() & { return err().value(); }
+ constexpr const E &&error() const && { return std::move(err().value()); }
+ TL_EXPECTED_11_CONSTEXPR E &&error() && { return std::move(err().value()); }
+
+ template <class U> constexpr T value_or(U &&v) const & {
+ static_assert(std::is_copy_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be copy-constructible and convertible to from U&&");
+ return bool(*this) ? **this : static_cast<T>(std::forward<U>(v));
+ }
+ template <class U> TL_EXPECTED_11_CONSTEXPR T value_or(U &&v) && {
+ static_assert(std::is_move_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be move-constructible and convertible to from U&&");
+ return bool(*this) ? std::move(**this) : static_cast<T>(std::forward<U>(v));
+ }
+};
+
+namespace detail {
+template <class Exp> using exp_t = typename detail::decay_t<Exp>::value_type;
+template <class Exp> using err_t = typename detail::decay_t<Exp>::error_type;
+template <class Exp, class Ret> using ret_t = expected<Ret, err_t<Exp>>;
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>()))>
+constexpr auto and_then_impl(Exp &&exp, F &&f) {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+ return exp.has_value()
+ ? detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp))
+ : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>()))>
+constexpr auto and_then_impl(Exp &&exp, F &&f) {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+ return exp.has_value() ? detail::invoke(std::forward<F>(f))
+ : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+#else
+template <class> struct TC;
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr>
+auto and_then_impl(Exp &&exp, F &&f) -> Ret {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+ return exp.has_value()
+ ? detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp))
+ : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr>
+constexpr auto and_then_impl(Exp &&exp, F &&f) -> Ret {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+ return exp.has_value() ? detail::invoke(std::forward<F>(f))
+ : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+#endif
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto expected_map_impl(Exp &&exp, F &&f) {
+ using result = ret_t<Exp, detail::decay_t<Ret>>;
+ return exp.has_value() ? result(detail::invoke(std::forward<F>(f),
+ *std::forward<Exp>(exp)))
+ : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto expected_map_impl(Exp &&exp, F &&f) {
+ using result = expected<void, err_t<Exp>>;
+ if (exp.has_value()) {
+ detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp));
+ return result();
+ }
+
+ return result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto expected_map_impl(Exp &&exp, F &&f) {
+ using result = ret_t<Exp, detail::decay_t<Ret>>;
+ return exp.has_value() ? result(detail::invoke(std::forward<F>(f)))
+ : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto expected_map_impl(Exp &&exp, F &&f) {
+ using result = expected<void, err_t<Exp>>;
+ if (exp.has_value()) {
+ detail::invoke(std::forward<F>(f));
+ return result();
+ }
+
+ return result(unexpect, std::forward<Exp>(exp).error());
+}
+#else
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto expected_map_impl(Exp &&exp, F &&f)
+ -> ret_t<Exp, detail::decay_t<Ret>> {
+ using result = ret_t<Exp, detail::decay_t<Ret>>;
+
+ return exp.has_value() ? result(detail::invoke(std::forward<F>(f),
+ *std::forward<Exp>(exp)))
+ : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto expected_map_impl(Exp &&exp, F &&f) -> expected<void, err_t<Exp>> {
+ if (exp.has_value()) {
+ detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp));
+ return {};
+ }
+
+ return unexpected<err_t<Exp>>(std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto expected_map_impl(Exp &&exp, F &&f)
+ -> ret_t<Exp, detail::decay_t<Ret>> {
+ using result = ret_t<Exp, detail::decay_t<Ret>>;
+
+ return exp.has_value() ? result(detail::invoke(std::forward<F>(f)))
+ : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto expected_map_impl(Exp &&exp, F &&f) -> expected<void, err_t<Exp>> {
+ if (exp.has_value()) {
+ detail::invoke(std::forward<F>(f));
+ return {};
+ }
+
+ return unexpected<err_t<Exp>>(std::forward<Exp>(exp).error());
+}
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f) {
+ using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+ return exp.has_value()
+ ? result(*std::forward<Exp>(exp))
+ : result(unexpect, detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()));
+}
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) {
+ using result = expected<exp_t<Exp>, monostate>;
+ if (exp.has_value()) {
+ return result(*std::forward<Exp>(exp));
+ }
+
+ detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+ return result(unexpect, monostate{});
+}
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f) {
+ using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+ return exp.has_value()
+ ? result()
+ : result(unexpect, detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()));
+}
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) {
+ using result = expected<exp_t<Exp>, monostate>;
+ if (exp.has_value()) {
+ return result();
+ }
+
+ detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+ return result(unexpect, monostate{});
+}
+#else
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f)
+ -> expected<exp_t<Exp>, detail::decay_t<Ret>> {
+ using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+
+ return exp.has_value()
+ ? result(*std::forward<Exp>(exp))
+ : result(unexpect, detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()));
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) -> expected<exp_t<Exp>, monostate> {
+ using result = expected<exp_t<Exp>, monostate>;
+ if (exp.has_value()) {
+ return result(*std::forward<Exp>(exp));
+ }
+
+ detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+ return result(unexpect, monostate{});
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f)
+ -> expected<exp_t<Exp>, detail::decay_t<Ret>> {
+ using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+
+ return exp.has_value()
+ ? result()
+ : result(unexpect, detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()));
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) -> expected<exp_t<Exp>, monostate> {
+ using result = expected<exp_t<Exp>, monostate>;
+ if (exp.has_value()) {
+ return result();
+ }
+
+ detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+ return result(unexpect, monostate{});
+}
+#endif
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto or_else_impl(Exp &&exp, F &&f) {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+ return exp.has_value()
+ ? std::forward<Exp>(exp)
+ : detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+detail::decay_t<Exp> or_else_impl(Exp &&exp, F &&f) {
+ return exp.has_value()
+ ? std::forward<Exp>(exp)
+ : (detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error()),
+ std::forward<Exp>(exp));
+}
+#else
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+auto or_else_impl(Exp &&exp, F &&f) -> Ret {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+ return exp.has_value()
+ ? std::forward<Exp>(exp)
+ : detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+detail::decay_t<Exp> or_else_impl(Exp &&exp, F &&f) {
+ return exp.has_value()
+ ? std::forward<Exp>(exp)
+ : (detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error()),
+ std::forward<Exp>(exp));
+}
+#endif
+} // namespace detail
+
+template <class T, class E, class U, class F>
+constexpr bool operator==(const expected<T, E> &lhs,
+ const expected<U, F> &rhs) {
+ return (lhs.has_value() != rhs.has_value())
+ ? false
+ : (!lhs.has_value() ? lhs.error() == rhs.error() : *lhs == *rhs);
+}
+template <class T, class E, class U, class F>
+constexpr bool operator!=(const expected<T, E> &lhs,
+ const expected<U, F> &rhs) {
+ return (lhs.has_value() != rhs.has_value())
+ ? true
+ : (!lhs.has_value() ? lhs.error() != rhs.error() : *lhs != *rhs);
+}
+
+template <class T, class E, class U>
+constexpr bool operator==(const expected<T, E> &x, const U &v) {
+ return x.has_value() ? *x == v : false;
+}
+template <class T, class E, class U>
+constexpr bool operator==(const U &v, const expected<T, E> &x) {
+ return x.has_value() ? *x == v : false;
+}
+template <class T, class E, class U>
+constexpr bool operator!=(const expected<T, E> &x, const U &v) {
+ return x.has_value() ? *x != v : true;
+}
+template <class T, class E, class U>
+constexpr bool operator!=(const U &v, const expected<T, E> &x) {
+ return x.has_value() ? *x != v : true;
+}
+
+template <class T, class E>
+constexpr bool operator==(const expected<T, E> &x, const unexpected<E> &e) {
+ return x.has_value() ? false : x.error() == e.value();
+}
+template <class T, class E>
+constexpr bool operator==(const unexpected<E> &e, const expected<T, E> &x) {
+ return x.has_value() ? false : x.error() == e.value();
+}
+template <class T, class E>
+constexpr bool operator!=(const expected<T, E> &x, const unexpected<E> &e) {
+ return x.has_value() ? true : x.error() != e.value();
+}
+template <class T, class E>
+constexpr bool operator!=(const unexpected<E> &e, const expected<T, E> &x) {
+ return x.has_value() ? true : x.error() != e.value();
+}
+
+template <class T, class E,
+ detail::enable_if_t<(std::is_void<T>::value ||
+ std::is_move_constructible<T>::value) &&
+ detail::is_swappable<T>::value &&
+ std::is_move_constructible<E>::value &&
+ detail::is_swappable<E>::value> * = nullptr>
+void swap(expected<T, E> &lhs,
+ expected<T, E> &rhs) noexcept(noexcept(lhs.swap(rhs))) {
+ lhs.swap(rhs);
+}
+} // namespace tl
+
+#endif
--- /dev/null
+
+///
+// optional - An implementation of std::optional with extensions
+// Written in 2017 by Sy Brand (tartanllama@gmail.com, @TartanLlama)
+//
+// Documentation available at https://tl.tartanllama.xyz/
+//
+// To the extent possible under law, the author(s) have dedicated all
+// copyright and related and neighboring rights to this software to the
+// public domain worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication
+// along with this software. If not, see
+// <http://creativecommons.org/publicdomain/zero/1.0/>.
+///
+
+#ifndef TL_OPTIONAL_HPP
+#define TL_OPTIONAL_HPP
+
+#define TL_OPTIONAL_VERSION_MAJOR 1
+#define TL_OPTIONAL_VERSION_MINOR 0
+#define TL_OPTIONAL_VERSION_PATCH 0
+
+#include <exception>
+#include <functional>
+#include <new>
+#include <type_traits>
+#include <utility>
+
+#if (defined(_MSC_VER) && _MSC_VER == 1900)
+#define TL_OPTIONAL_MSVC2015
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \
+ !defined(__clang__))
+#define TL_OPTIONAL_GCC49
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 && \
+ !defined(__clang__))
+#define TL_OPTIONAL_GCC54
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 && \
+ !defined(__clang__))
+#define TL_OPTIONAL_GCC55
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \
+ !defined(__clang__))
+// GCC < 5 doesn't support overloading on const&& for member functions
+#define TL_OPTIONAL_NO_CONSTRR
+
+// GCC < 5 doesn't support some standard C++11 type traits
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ std::has_trivial_copy_constructor<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) std::has_trivial_copy_assign<T>::value
+
+// This one will be different for GCC 5.7 if it's ever supported
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+
+// GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks std::vector
+// for non-copyable types
+#elif (defined(__GNUC__) && __GNUC__ < 8 && \
+ !defined(__clang__))
+#ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+#define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+namespace tl {
+ namespace detail {
+ template<class T>
+ struct is_trivially_copy_constructible : std::is_trivially_copy_constructible<T>{};
+#ifdef _GLIBCXX_VECTOR
+ template<class T, class A>
+ struct is_trivially_copy_constructible<std::vector<T,A>>
+ : std::is_trivially_copy_constructible<T>{};
+#endif
+ }
+}
+#endif
+
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ tl::detail::is_trivially_copy_constructible<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::is_trivially_copy_assignable<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+#else
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ std::is_trivially_copy_constructible<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::is_trivially_copy_assignable<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+#endif
+
+#if __cplusplus > 201103L
+#define TL_OPTIONAL_CXX14
+#endif
+
+// constexpr implies const in C++11, not C++14
+#if (__cplusplus == 201103L || defined(TL_OPTIONAL_MSVC2015) || \
+ defined(TL_OPTIONAL_GCC49))
+#define TL_OPTIONAL_11_CONSTEXPR
+#else
+#define TL_OPTIONAL_11_CONSTEXPR constexpr
+#endif
+
+namespace tl {
+#ifndef TL_MONOSTATE_INPLACE_MUTEX
+#define TL_MONOSTATE_INPLACE_MUTEX
+/// Used to represent an optional with no data; essentially a bool
+class monostate {};
+
+/// A tag type to tell optional to construct its value in-place
+struct in_place_t {
+ explicit in_place_t() = default;
+};
+/// A tag to tell optional to construct its value in-place
+static constexpr in_place_t in_place{};
+#endif
+
+template <class T> class optional;
+
+namespace detail {
+#ifndef TL_TRAITS_MUTEX
+#define TL_TRAITS_MUTEX
+// C++14-style aliases for brevity
+template <class T> using remove_const_t = typename std::remove_const<T>::type;
+template <class T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template <class T> using decay_t = typename std::decay<T>::type;
+template <bool E, class T = void>
+using enable_if_t = typename std::enable_if<E, T>::type;
+template <bool B, class T, class F>
+using conditional_t = typename std::conditional<B, T, F>::type;
+
+// std::conjunction from C++17
+template <class...> struct conjunction : std::true_type {};
+template <class B> struct conjunction<B> : B {};
+template <class B, class... Bs>
+struct conjunction<B, Bs...>
+ : std::conditional<bool(B::value), conjunction<Bs...>, B>::type {};
+
+#if defined(_LIBCPP_VERSION) && __cplusplus == 201103L
+#define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+#endif
+
+// In C++11 mode, there's an issue in libc++'s std::mem_fn
+// which results in a hard-error when using it in a noexcept expression
+// in some cases. This is a check to workaround the common failing case.
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+template <class T> struct is_pointer_to_non_const_member_func : std::false_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)&> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)&&> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile&> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile&&> : std::true_type{};
+
+template <class T> struct is_const_or_const_ref : std::false_type{};
+template <class T> struct is_const_or_const_ref<T const&> : std::true_type{};
+template <class T> struct is_const_or_const_ref<T const> : std::true_type{};
+#endif
+
+// std::invoke from C++17
+// https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround
+template <typename Fn, typename... Args,
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+ typename = enable_if_t<!(is_pointer_to_non_const_member_func<Fn>::value
+ && is_const_or_const_ref<Args...>::value)>,
+#endif
+ typename = enable_if_t<std::is_member_pointer<decay_t<Fn>>::value>,
+ int = 0>
+constexpr auto invoke(Fn &&f, Args &&... args) noexcept(
+ noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+ -> decltype(std::mem_fn(f)(std::forward<Args>(args)...)) {
+ return std::mem_fn(f)(std::forward<Args>(args)...);
+}
+
+template <typename Fn, typename... Args,
+ typename = enable_if_t<!std::is_member_pointer<decay_t<Fn>>::value>>
+constexpr auto invoke(Fn &&f, Args &&... args) noexcept(
+ noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+ -> decltype(std::forward<Fn>(f)(std::forward<Args>(args)...)) {
+ return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+// std::invoke_result from C++17
+template <class F, class, class... Us> struct invoke_result_impl;
+
+template <class F, class... Us>
+struct invoke_result_impl<
+ F, decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...), void()),
+ Us...> {
+ using type = decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...));
+};
+
+template <class F, class... Us>
+using invoke_result = invoke_result_impl<F, void, Us...>;
+
+template <class F, class... Us>
+using invoke_result_t = typename invoke_result<F, Us...>::type;
+
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+// TODO make a version which works with MSVC 2015
+template <class T, class U = T> struct is_swappable : std::true_type {};
+
+template <class T, class U = T> struct is_nothrow_swappable : std::true_type {};
+#else
+// https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept
+namespace swap_adl_tests {
+// if swap ADL finds this then it would call std::swap otherwise (same
+// signature)
+struct tag {};
+
+template <class T> tag swap(T &, T &);
+template <class T, std::size_t N> tag swap(T (&a)[N], T (&b)[N]);
+
+// helper functions to test if an unqualified swap is possible, and if it
+// becomes std::swap
+template <class, class> std::false_type can_swap(...) noexcept(false);
+template <class T, class U,
+ class = decltype(swap(std::declval<T &>(), std::declval<U &>()))>
+std::true_type can_swap(int) noexcept(noexcept(swap(std::declval<T &>(),
+ std::declval<U &>())));
+
+template <class, class> std::false_type uses_std(...);
+template <class T, class U>
+std::is_same<decltype(swap(std::declval<T &>(), std::declval<U &>())), tag>
+uses_std(int);
+
+template <class T>
+struct is_std_swap_noexcept
+ : std::integral_constant<bool,
+ std::is_nothrow_move_constructible<T>::value &&
+ std::is_nothrow_move_assignable<T>::value> {};
+
+template <class T, std::size_t N>
+struct is_std_swap_noexcept<T[N]> : is_std_swap_noexcept<T> {};
+
+template <class T, class U>
+struct is_adl_swap_noexcept
+ : std::integral_constant<bool, noexcept(can_swap<T, U>(0))> {};
+} // namespace swap_adl_tests
+
+template <class T, class U = T>
+struct is_swappable
+ : std::integral_constant<
+ bool,
+ decltype(detail::swap_adl_tests::can_swap<T, U>(0))::value &&
+ (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value ||
+ (std::is_move_assignable<T>::value &&
+ std::is_move_constructible<T>::value))> {};
+
+template <class T, std::size_t N>
+struct is_swappable<T[N], T[N]>
+ : std::integral_constant<
+ bool,
+ decltype(detail::swap_adl_tests::can_swap<T[N], T[N]>(0))::value &&
+ (!decltype(
+ detail::swap_adl_tests::uses_std<T[N], T[N]>(0))::value ||
+ is_swappable<T, T>::value)> {};
+
+template <class T, class U = T>
+struct is_nothrow_swappable
+ : std::integral_constant<
+ bool,
+ is_swappable<T, U>::value &&
+ ((decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value
+ &&detail::swap_adl_tests::is_std_swap_noexcept<T>::value) ||
+ (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
+ detail::swap_adl_tests::is_adl_swap_noexcept<T,
+ U>::value))> {
+};
+#endif
+#endif
+
+// std::void_t from C++17
+template <class...> struct voider { using type = void; };
+template <class... Ts> using void_t = typename voider<Ts...>::type;
+
+// Trait for checking if a type is a tl::optional
+template <class T> struct is_optional_impl : std::false_type {};
+template <class T> struct is_optional_impl<optional<T>> : std::true_type {};
+template <class T> using is_optional = is_optional_impl<decay_t<T>>;
+
+// Change void to tl::monostate
+template <class U>
+using fixup_void = conditional_t<std::is_void<U>::value, monostate, U>;
+
+template <class F, class U, class = invoke_result_t<F, U>>
+using get_map_return = optional<fixup_void<invoke_result_t<F, U>>>;
+
+// Check if invoking F for some Us returns void
+template <class F, class = void, class... U> struct returns_void_impl;
+template <class F, class... U>
+struct returns_void_impl<F, void_t<invoke_result_t<F, U...>>, U...>
+ : std::is_void<invoke_result_t<F, U...>> {};
+template <class F, class... U>
+using returns_void = returns_void_impl<F, void, U...>;
+
+template <class T, class... U>
+using enable_if_ret_void = enable_if_t<returns_void<T &&, U...>::value>;
+
+template <class T, class... U>
+using disable_if_ret_void = enable_if_t<!returns_void<T &&, U...>::value>;
+
+template <class T, class U>
+using enable_forward_value =
+ detail::enable_if_t<std::is_constructible<T, U &&>::value &&
+ !std::is_same<detail::decay_t<U>, in_place_t>::value &&
+ !std::is_same<optional<T>, detail::decay_t<U>>::value>;
+
+template <class T, class U, class Other>
+using enable_from_other = detail::enable_if_t<
+ std::is_constructible<T, Other>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, const optional<U> &>::value &&
+ !std::is_constructible<T, const optional<U> &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<const optional<U> &, T>::value &&
+ !std::is_convertible<const optional<U> &&, T>::value>;
+
+template <class T, class U>
+using enable_assign_forward = detail::enable_if_t<
+ !std::is_same<optional<T>, detail::decay_t<U>>::value &&
+ !detail::conjunction<std::is_scalar<T>,
+ std::is_same<T, detail::decay_t<U>>>::value &&
+ std::is_constructible<T, U>::value && std::is_assignable<T &, U>::value>;
+
+template <class T, class U, class Other>
+using enable_assign_from_other = detail::enable_if_t<
+ std::is_constructible<T, Other>::value &&
+ std::is_assignable<T &, Other>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, const optional<U> &>::value &&
+ !std::is_constructible<T, const optional<U> &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<const optional<U> &, T>::value &&
+ !std::is_convertible<const optional<U> &&, T>::value &&
+ !std::is_assignable<T &, optional<U> &>::value &&
+ !std::is_assignable<T &, optional<U> &&>::value &&
+ !std::is_assignable<T &, const optional<U> &>::value &&
+ !std::is_assignable<T &, const optional<U> &&>::value>;
+
+// The storage base manages the actual storage, and correctly propagates
+// trivial destruction from T. This case is for when T is not trivially
+// destructible.
+template <class T, bool = ::std::is_trivially_destructible<T>::value>
+struct optional_storage_base {
+ TL_OPTIONAL_11_CONSTEXPR optional_storage_base() noexcept
+ : m_dummy(), m_has_value(false) {}
+
+ template <class... U>
+ TL_OPTIONAL_11_CONSTEXPR optional_storage_base(in_place_t, U &&... u)
+ : m_value(std::forward<U>(u)...), m_has_value(true) {}
+
+ ~optional_storage_base() {
+ if (m_has_value) {
+ m_value.~T();
+ m_has_value = false;
+ }
+ }
+
+ struct dummy {};
+ union {
+ dummy m_dummy;
+ T m_value;
+ };
+
+ bool m_has_value;
+};
+
+// This case is for when T is trivially destructible.
+template <class T> struct optional_storage_base<T, true> {
+ TL_OPTIONAL_11_CONSTEXPR optional_storage_base() noexcept
+ : m_dummy(), m_has_value(false) {}
+
+ template <class... U>
+ TL_OPTIONAL_11_CONSTEXPR optional_storage_base(in_place_t, U &&... u)
+ : m_value(std::forward<U>(u)...), m_has_value(true) {}
+
+ // No destructor, so this class is trivially destructible
+
+ struct dummy {};
+ union {
+ dummy m_dummy;
+ T m_value;
+ };
+
+ bool m_has_value = false;
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class T> struct optional_operations_base : optional_storage_base<T> {
+ using optional_storage_base<T>::optional_storage_base;
+
+ void hard_reset() noexcept {
+ get().~T();
+ this->m_has_value = false;
+ }
+
+ template <class... Args> void construct(Args &&... args) noexcept {
+ new (std::addressof(this->m_value)) T(std::forward<Args>(args)...);
+ this->m_has_value = true;
+ }
+
+ template <class Opt> void assign(Opt &&rhs) {
+ if (this->has_value()) {
+ if (rhs.has_value()) {
+ this->m_value = std::forward<Opt>(rhs).get();
+ } else {
+ this->m_value.~T();
+ this->m_has_value = false;
+ }
+ }
+
+ else if (rhs.has_value()) {
+ construct(std::forward<Opt>(rhs).get());
+ }
+ }
+
+ bool has_value() const { return this->m_has_value; }
+
+ TL_OPTIONAL_11_CONSTEXPR T &get() & { return this->m_value; }
+ TL_OPTIONAL_11_CONSTEXPR const T &get() const & { return this->m_value; }
+ TL_OPTIONAL_11_CONSTEXPR T &&get() && { return std::move(this->m_value); }
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr const T &&get() const && { return std::move(this->m_value); }
+#endif
+};
+
+// This class manages conditionally having a trivial copy constructor
+// This specialization is for when T is trivially copy constructible
+template <class T, bool = TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)>
+struct optional_copy_base : optional_operations_base<T> {
+ using optional_operations_base<T>::optional_operations_base;
+};
+
+// This specialization is for when T is not trivially copy constructible
+template <class T>
+struct optional_copy_base<T, false> : optional_operations_base<T> {
+ using optional_operations_base<T>::optional_operations_base;
+
+ optional_copy_base() = default;
+ optional_copy_base(const optional_copy_base &rhs)
+ : optional_operations_base<T>() {
+ if (rhs.has_value()) {
+ this->construct(rhs.get());
+ } else {
+ this->m_has_value = false;
+ }
+ }
+
+ optional_copy_base(optional_copy_base &&rhs) = default;
+ optional_copy_base &operator=(const optional_copy_base &rhs) = default;
+ optional_copy_base &operator=(optional_copy_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move constructor
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_constructible. We
+// have to make do with a non-trivial move constructor even if T is trivially
+// move constructible
+#ifndef TL_OPTIONAL_GCC49
+template <class T, bool = std::is_trivially_move_constructible<T>::value>
+struct optional_move_base : optional_copy_base<T> {
+ using optional_copy_base<T>::optional_copy_base;
+};
+#else
+template <class T, bool = false> struct optional_move_base;
+#endif
+template <class T> struct optional_move_base<T, false> : optional_copy_base<T> {
+ using optional_copy_base<T>::optional_copy_base;
+
+ optional_move_base() = default;
+ optional_move_base(const optional_move_base &rhs) = default;
+
+ optional_move_base(optional_move_base &&rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value) {
+ if (rhs.has_value()) {
+ this->construct(std::move(rhs.get()));
+ } else {
+ this->m_has_value = false;
+ }
+ }
+ optional_move_base &operator=(const optional_move_base &rhs) = default;
+ optional_move_base &operator=(optional_move_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial copy assignment operator
+template <class T, bool = TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) &&
+ TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) &&
+ TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T)>
+struct optional_copy_assign_base : optional_move_base<T> {
+ using optional_move_base<T>::optional_move_base;
+};
+
+template <class T>
+struct optional_copy_assign_base<T, false> : optional_move_base<T> {
+ using optional_move_base<T>::optional_move_base;
+
+ optional_copy_assign_base() = default;
+ optional_copy_assign_base(const optional_copy_assign_base &rhs) = default;
+
+ optional_copy_assign_base(optional_copy_assign_base &&rhs) = default;
+ optional_copy_assign_base &operator=(const optional_copy_assign_base &rhs) {
+ this->assign(rhs);
+ return *this;
+ }
+ optional_copy_assign_base &
+ operator=(optional_copy_assign_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move assignment operator
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_assignable. We have
+// to make do with a non-trivial move assignment operator even if T is trivially
+// move assignable
+#ifndef TL_OPTIONAL_GCC49
+template <class T, bool = std::is_trivially_destructible<T>::value
+ &&std::is_trivially_move_constructible<T>::value
+ &&std::is_trivially_move_assignable<T>::value>
+struct optional_move_assign_base : optional_copy_assign_base<T> {
+ using optional_copy_assign_base<T>::optional_copy_assign_base;
+};
+#else
+template <class T, bool = false> struct optional_move_assign_base;
+#endif
+
+template <class T>
+struct optional_move_assign_base<T, false> : optional_copy_assign_base<T> {
+ using optional_copy_assign_base<T>::optional_copy_assign_base;
+
+ optional_move_assign_base() = default;
+ optional_move_assign_base(const optional_move_assign_base &rhs) = default;
+
+ optional_move_assign_base(optional_move_assign_base &&rhs) = default;
+
+ optional_move_assign_base &
+ operator=(const optional_move_assign_base &rhs) = default;
+
+ optional_move_assign_base &
+ operator=(optional_move_assign_base &&rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value
+ &&std::is_nothrow_move_assignable<T>::value) {
+ this->assign(std::move(rhs));
+ return *this;
+ }
+};
+
+// optional_delete_ctor_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible
+template <class T, bool EnableCopy = std::is_copy_constructible<T>::value,
+ bool EnableMove = std::is_move_constructible<T>::value>
+struct optional_delete_ctor_base {
+ optional_delete_ctor_base() = default;
+ optional_delete_ctor_base(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = default;
+ optional_delete_ctor_base &
+ operator=(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base &
+ operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, true, false> {
+ optional_delete_ctor_base() = default;
+ optional_delete_ctor_base(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = delete;
+ optional_delete_ctor_base &
+ operator=(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base &
+ operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, false, true> {
+ optional_delete_ctor_base() = default;
+ optional_delete_ctor_base(const optional_delete_ctor_base &) = delete;
+ optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = default;
+ optional_delete_ctor_base &
+ operator=(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base &
+ operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, false, false> {
+ optional_delete_ctor_base() = default;
+ optional_delete_ctor_base(const optional_delete_ctor_base &) = delete;
+ optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = delete;
+ optional_delete_ctor_base &
+ operator=(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base &
+ operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+// optional_delete_assign_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible + assignable
+template <class T,
+ bool EnableCopy = (std::is_copy_constructible<T>::value &&
+ std::is_copy_assignable<T>::value),
+ bool EnableMove = (std::is_move_constructible<T>::value &&
+ std::is_move_assignable<T>::value)>
+struct optional_delete_assign_base {
+ optional_delete_assign_base() = default;
+ optional_delete_assign_base(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+ default;
+ optional_delete_assign_base &
+ operator=(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base &
+ operator=(optional_delete_assign_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_assign_base<T, true, false> {
+ optional_delete_assign_base() = default;
+ optional_delete_assign_base(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+ default;
+ optional_delete_assign_base &
+ operator=(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base &
+ operator=(optional_delete_assign_base &&) noexcept = delete;
+};
+
+template <class T> struct optional_delete_assign_base<T, false, true> {
+ optional_delete_assign_base() = default;
+ optional_delete_assign_base(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+ default;
+ optional_delete_assign_base &
+ operator=(const optional_delete_assign_base &) = delete;
+ optional_delete_assign_base &
+ operator=(optional_delete_assign_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_assign_base<T, false, false> {
+ optional_delete_assign_base() = default;
+ optional_delete_assign_base(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+ default;
+ optional_delete_assign_base &
+ operator=(const optional_delete_assign_base &) = delete;
+ optional_delete_assign_base &
+ operator=(optional_delete_assign_base &&) noexcept = delete;
+};
+
+} // namespace detail
+
+/// A tag type to represent an empty optional
+struct nullopt_t {
+ struct do_not_use {};
+ constexpr explicit nullopt_t(do_not_use, do_not_use) noexcept {}
+};
+/// Represents an empty optional
+static constexpr nullopt_t nullopt{nullopt_t::do_not_use{},
+ nullopt_t::do_not_use{}};
+
+class bad_optional_access : public std::exception {
+public:
+ bad_optional_access() = default;
+ const char *what() const noexcept { return "Optional has no value"; }
+};
+
+/// An optional object is an object that contains the storage for another
+/// object and manages the lifetime of this contained object, if any. The
+/// contained object may be initialized after the optional object has been
+/// initialized, and may be destroyed before the optional object has been
+/// destroyed. The initialization state of the contained object is tracked by
+/// the optional object.
+template <class T>
+class optional : private detail::optional_move_assign_base<T>,
+ private detail::optional_delete_ctor_base<T>,
+ private detail::optional_delete_assign_base<T> {
+ using base = detail::optional_move_assign_base<T>;
+
+ static_assert(!std::is_same<T, in_place_t>::value,
+ "instantiation of optional with in_place_t is ill-formed");
+ static_assert(!std::is_same<detail::decay_t<T>, nullopt_t>::value,
+ "instantiation of optional with nullopt_t is ill-formed");
+
+public:
+// The different versions for C++14 and 11 are needed because deduced return
+// types are not SFINAE-safe. This provides better support for things like
+// generic lambdas. C.f.
+// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0826r0.html
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation which returns an optional on the stored
+ /// object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) & {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) && {
+ using result = detail::invoke_result_t<F, T &&>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+
+ template <class F> constexpr auto and_then(F &&f) const & {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F> constexpr auto and_then(F &&f) const && {
+ using result = detail::invoke_result_t<F, const T &&>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+#endif
+#else
+ /// Carries out some operation which returns an optional on the stored
+ /// object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) & {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &&> and_then(F &&f) && {
+ using result = detail::invoke_result_t<F, T &&>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+
+ template <class F>
+ constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const & {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr detail::invoke_result_t<F, const T &&> and_then(F &&f) const && {
+ using result = detail::invoke_result_t<F, const T &&>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation on the stored object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto map(F &&f) const & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto map(F &&f) const && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ /// Carries out some operation on the stored object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional &>(),
+ std::declval<F &&>()))
+ map(F &&f) & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional &&>(),
+ std::declval<F &&>()))
+ map(F &&f) && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F>
+ constexpr decltype(optional_map_impl(std::declval<const optional &>(),
+ std::declval<F &&>()))
+ map(F &&f) const & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr decltype(optional_map_impl(std::declval<const optional &&>(),
+ std::declval<F &&>()))
+ map(F &&f) const && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation on the stored object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto transform(F&& f) const & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto transform(F&& f) const && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ /// Carries out some operation on the stored object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional&>(),
+ std::declval<F&&>()))
+ transform(F&& f) & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional&&>(),
+ std::declval<F&&>()))
+ transform(F&& f) && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F>
+ constexpr decltype(optional_map_impl(std::declval<const optional&>(),
+ std::declval<F&&>()))
+ transform(F&& f) const & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr decltype(optional_map_impl(std::declval<const optional&&>(),
+ std::declval<F&&>()))
+ transform(F&& f) const && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+ /// Calls `f` if the optional is empty
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+ if (has_value())
+ return *this;
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+ return has_value() ? *this : std::forward<F>(f)();
+ }
+
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) && {
+ if (has_value())
+ return std::move(*this);
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) && {
+ return has_value() ? std::move(*this) : std::forward<F>(f)();
+ }
+
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const & {
+ if (has_value())
+ return *this;
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) const & {
+ return has_value() ? *this : std::forward<F>(f)();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const && {
+ if (has_value())
+ return std::move(*this);
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const && {
+ return has_value() ? std::move(*this) : std::forward<F>(f)();
+ }
+#endif
+
+ /// Maps the stored value with `f` if there is one, otherwise returns `u`.
+ template <class F, class U> U map_or(F &&f, U &&u) & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u);
+ }
+
+ template <class F, class U> U map_or(F &&f, U &&u) && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u);
+ }
+
+ template <class F, class U> U map_or(F &&f, U &&u) const & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, class U> U map_or(F &&f, U &&u) const && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u);
+ }
+#endif
+
+ /// Maps the stored value with `f` if there is one, otherwise calls
+ /// `u` and returns the result.
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u)();
+ }
+
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u)();
+ }
+
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u)();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u)();
+ }
+#endif
+
+ /// Returns `u` if `*this` has a value, otherwise an empty optional.
+ template <class U>
+ constexpr optional<typename std::decay<U>::type> conjunction(U &&u) const {
+ using result = optional<detail::decay_t<U>>;
+ return has_value() ? result{u} : result{nullopt};
+ }
+
+ /// Returns `rhs` if `*this` is empty, otherwise the current value.
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) & {
+ return has_value() ? *this : rhs;
+ }
+
+ constexpr optional disjunction(const optional &rhs) const & {
+ return has_value() ? *this : rhs;
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) && {
+ return has_value() ? std::move(*this) : rhs;
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr optional disjunction(const optional &rhs) const && {
+ return has_value() ? std::move(*this) : rhs;
+ }
+#endif
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) & {
+ return has_value() ? *this : std::move(rhs);
+ }
+
+ constexpr optional disjunction(optional &&rhs) const & {
+ return has_value() ? *this : std::move(rhs);
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) && {
+ return has_value() ? std::move(*this) : std::move(rhs);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr optional disjunction(optional &&rhs) const && {
+ return has_value() ? std::move(*this) : std::move(rhs);
+ }
+#endif
+
+ /// Takes the value out of the optional, leaving it empty
+ optional take() {
+ optional ret = std::move(*this);
+ reset();
+ return ret;
+ }
+
+ using value_type = T;
+
+ /// Constructs an optional that does not contain a value.
+ constexpr optional() noexcept = default;
+
+ constexpr optional(nullopt_t) noexcept {}
+
+ /// Copy constructor
+ ///
+ /// If `rhs` contains a value, the stored value is direct-initialized with
+ /// it. Otherwise, the constructed optional is empty.
+ TL_OPTIONAL_11_CONSTEXPR optional(const optional &rhs) = default;
+
+ /// Move constructor
+ ///
+ /// If `rhs` contains a value, the stored value is direct-initialized with
+ /// it. Otherwise, the constructed optional is empty.
+ TL_OPTIONAL_11_CONSTEXPR optional(optional &&rhs) = default;
+
+ /// Constructs the stored value in-place using the given arguments.
+ template <class... Args>
+ constexpr explicit optional(
+ detail::enable_if_t<std::is_constructible<T, Args...>::value, in_place_t>,
+ Args &&... args)
+ : base(in_place, std::forward<Args>(args)...) {}
+
+ template <class U, class... Args>
+ TL_OPTIONAL_11_CONSTEXPR explicit optional(
+ detail::enable_if_t<std::is_constructible<T, std::initializer_list<U> &,
+ Args &&...>::value,
+ in_place_t>,
+ std::initializer_list<U> il, Args &&... args) {
+ this->construct(il, std::forward<Args>(args)...);
+ }
+
+ /// Constructs the stored value with `u`.
+ template <
+ class U = T,
+ detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr,
+ detail::enable_forward_value<T, U> * = nullptr>
+ constexpr optional(U &&u) : base(in_place, std::forward<U>(u)) {}
+
+ template <
+ class U = T,
+ detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr,
+ detail::enable_forward_value<T, U> * = nullptr>
+ constexpr explicit optional(U &&u) : base(in_place, std::forward<U>(u)) {}
+
+ /// Converting copy constructor.
+ template <
+ class U, detail::enable_from_other<T, U, const U &> * = nullptr,
+ detail::enable_if_t<std::is_convertible<const U &, T>::value> * = nullptr>
+ optional(const optional<U> &rhs) {
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ }
+ }
+
+ template <class U, detail::enable_from_other<T, U, const U &> * = nullptr,
+ detail::enable_if_t<!std::is_convertible<const U &, T>::value> * =
+ nullptr>
+ explicit optional(const optional<U> &rhs) {
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ }
+ }
+
+ /// Converting move constructor.
+ template <
+ class U, detail::enable_from_other<T, U, U &&> * = nullptr,
+ detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr>
+ optional(optional<U> &&rhs) {
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ }
+ }
+
+ template <
+ class U, detail::enable_from_other<T, U, U &&> * = nullptr,
+ detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr>
+ explicit optional(optional<U> &&rhs) {
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ }
+ }
+
+ /// Destroys the stored value if there is one.
+ ~optional() = default;
+
+ /// Assignment to empty.
+ ///
+ /// Destroys the current value if there is one.
+ optional &operator=(nullopt_t) noexcept {
+ if (has_value()) {
+ this->m_value.~T();
+ this->m_has_value = false;
+ }
+
+ return *this;
+ }
+
+ /// Copy assignment.
+ ///
+ /// Copies the value from `rhs` if there is one. Otherwise resets the stored
+ /// value in `*this`.
+ optional &operator=(const optional &rhs) = default;
+
+ /// Move assignment.
+ ///
+ /// Moves the value from `rhs` if there is one. Otherwise resets the stored
+ /// value in `*this`.
+ optional &operator=(optional &&rhs) = default;
+
+ /// Assigns the stored value from `u`, destroying the old value if there was
+ /// one.
+ template <class U = T, detail::enable_assign_forward<T, U> * = nullptr>
+ optional &operator=(U &&u) {
+ if (has_value()) {
+ this->m_value = std::forward<U>(u);
+ } else {
+ this->construct(std::forward<U>(u));
+ }
+
+ return *this;
+ }
+
+ /// Converting copy assignment operator.
+ ///
+ /// Copies the value from `rhs` if there is one. Otherwise resets the stored
+ /// value in `*this`.
+ template <class U,
+ detail::enable_assign_from_other<T, U, const U &> * = nullptr>
+ optional &operator=(const optional<U> &rhs) {
+ if (has_value()) {
+ if (rhs.has_value()) {
+ this->m_value = *rhs;
+ } else {
+ this->hard_reset();
+ }
+ }
+
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ }
+
+ return *this;
+ }
+
+ // TODO check exception guarantee
+ /// Converting move assignment operator.
+ ///
+ /// Moves the value from `rhs` if there is one. Otherwise resets the stored
+ /// value in `*this`.
+ template <class U, detail::enable_assign_from_other<T, U, U> * = nullptr>
+ optional &operator=(optional<U> &&rhs) {
+ if (has_value()) {
+ if (rhs.has_value()) {
+ this->m_value = std::move(*rhs);
+ } else {
+ this->hard_reset();
+ }
+ }
+
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ }
+
+ return *this;
+ }
+
+ /// Constructs the value in-place, destroying the current one if there is
+ /// one.
+ template <class... Args> T &emplace(Args &&... args) {
+ static_assert(std::is_constructible<T, Args &&...>::value,
+ "T must be constructible with Args");
+
+ *this = nullopt;
+ this->construct(std::forward<Args>(args)...);
+ return value();
+ }
+
+ template <class U, class... Args>
+ detail::enable_if_t<
+ std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value,
+ T &>
+ emplace(std::initializer_list<U> il, Args &&... args) {
+ *this = nullopt;
+ this->construct(il, std::forward<Args>(args)...);
+ return value();
+ }
+
+ /// Swaps this optional with the other.
+ ///
+ /// If neither optionals have a value, nothing happens.
+ /// If both have a value, the values are swapped.
+ /// If one has a value, it is moved to the other and the movee is left
+ /// valueless.
+ void
+ swap(optional &rhs) noexcept(std::is_nothrow_move_constructible<T>::value
+ &&detail::is_nothrow_swappable<T>::value) {
+ using std::swap;
+ if (has_value()) {
+ if (rhs.has_value()) {
+ swap(**this, *rhs);
+ } else {
+ new (std::addressof(rhs.m_value)) T(std::move(this->m_value));
+ this->m_value.T::~T();
+ }
+ } else if (rhs.has_value()) {
+ new (std::addressof(this->m_value)) T(std::move(rhs.m_value));
+ rhs.m_value.T::~T();
+ }
+ swap(this->m_has_value, rhs.m_has_value);
+ }
+
+ /// Returns a pointer to the stored value
+ constexpr const T *operator->() const {
+ return std::addressof(this->m_value);
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR T *operator->() {
+ return std::addressof(this->m_value);
+ }
+
+ /// Returns the stored value
+ TL_OPTIONAL_11_CONSTEXPR T &operator*() & { return this->m_value; }
+
+ constexpr const T &operator*() const & { return this->m_value; }
+
+ TL_OPTIONAL_11_CONSTEXPR T &&operator*() && {
+ return std::move(this->m_value);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr const T &&operator*() const && { return std::move(this->m_value); }
+#endif
+
+ /// Returns whether or not the optional has a value
+ constexpr bool has_value() const noexcept { return this->m_has_value; }
+
+ constexpr explicit operator bool() const noexcept {
+ return this->m_has_value;
+ }
+
+ /// Returns the contained value if there is one, otherwise throws bad_optional_access
+ TL_OPTIONAL_11_CONSTEXPR T &value() & {
+ if (has_value())
+ return this->m_value;
+ throw bad_optional_access();
+ }
+ TL_OPTIONAL_11_CONSTEXPR const T &value() const & {
+ if (has_value())
+ return this->m_value;
+ throw bad_optional_access();
+ }
+ TL_OPTIONAL_11_CONSTEXPR T &&value() && {
+ if (has_value())
+ return std::move(this->m_value);
+ throw bad_optional_access();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ TL_OPTIONAL_11_CONSTEXPR const T &&value() const && {
+ if (has_value())
+ return std::move(this->m_value);
+ throw bad_optional_access();
+ }
+#endif
+
+ /// Returns the stored value if there is one, otherwise returns `u`
+ template <class U> constexpr T value_or(U &&u) const & {
+ static_assert(std::is_copy_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be copy constructible and convertible from U");
+ return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+ }
+
+ template <class U> TL_OPTIONAL_11_CONSTEXPR T value_or(U &&u) && {
+ static_assert(std::is_move_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be move constructible and convertible from U");
+ return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+ }
+
+ /// Destroys the stored value if one exists, making the optional empty
+ void reset() noexcept {
+ if (has_value()) {
+ this->m_value.~T();
+ this->m_has_value = false;
+ }
+ }
+}; // namespace tl
+
+/// Compares two optional objects
+template <class T, class U>
+inline constexpr bool operator==(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return lhs.has_value() == rhs.has_value() &&
+ (!lhs.has_value() || *lhs == *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator!=(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return lhs.has_value() != rhs.has_value() ||
+ (lhs.has_value() && *lhs != *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator<(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return rhs.has_value() && (!lhs.has_value() || *lhs < *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator>(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return lhs.has_value() && (!rhs.has_value() || *lhs > *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator<=(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return !lhs.has_value() || (rhs.has_value() && *lhs <= *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator>=(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return !rhs.has_value() || (lhs.has_value() && *lhs >= *rhs);
+}
+
+/// Compares an optional to a `nullopt`
+template <class T>
+inline constexpr bool operator==(const optional<T> &lhs, nullopt_t) noexcept {
+ return !lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator==(nullopt_t, const optional<T> &rhs) noexcept {
+ return !rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator!=(const optional<T> &lhs, nullopt_t) noexcept {
+ return lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator!=(nullopt_t, const optional<T> &rhs) noexcept {
+ return rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<(const optional<T> &, nullopt_t) noexcept {
+ return false;
+}
+template <class T>
+inline constexpr bool operator<(nullopt_t, const optional<T> &rhs) noexcept {
+ return rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<=(const optional<T> &lhs, nullopt_t) noexcept {
+ return !lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<=(nullopt_t, const optional<T> &) noexcept {
+ return true;
+}
+template <class T>
+inline constexpr bool operator>(const optional<T> &lhs, nullopt_t) noexcept {
+ return lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator>(nullopt_t, const optional<T> &) noexcept {
+ return false;
+}
+template <class T>
+inline constexpr bool operator>=(const optional<T> &, nullopt_t) noexcept {
+ return true;
+}
+template <class T>
+inline constexpr bool operator>=(nullopt_t, const optional<T> &rhs) noexcept {
+ return !rhs.has_value();
+}
+
+/// Compares the optional with a value.
+template <class T, class U>
+inline constexpr bool operator==(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs == rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator==(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs == *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator!=(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs != rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator!=(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs != *rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs < rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs < *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator<=(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs <= rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<=(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs <= *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs > rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs > *rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator>=(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs >= rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>=(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs >= *rhs : true;
+}
+
+template <class T,
+ detail::enable_if_t<std::is_move_constructible<T>::value> * = nullptr,
+ detail::enable_if_t<detail::is_swappable<T>::value> * = nullptr>
+void swap(optional<T> &lhs,
+ optional<T> &rhs) noexcept(noexcept(lhs.swap(rhs))) {
+ return lhs.swap(rhs);
+}
+
+namespace detail {
+struct i_am_secret {};
+} // namespace detail
+
+template <class T = detail::i_am_secret, class U,
+ class Ret =
+ detail::conditional_t<std::is_same<T, detail::i_am_secret>::value,
+ detail::decay_t<U>, T>>
+inline constexpr optional<Ret> make_optional(U &&v) {
+ return optional<Ret>(std::forward<U>(v));
+}
+
+template <class T, class... Args>
+inline constexpr optional<T> make_optional(Args &&... args) {
+ return optional<T>(in_place, std::forward<Args>(args)...);
+}
+template <class T, class U, class... Args>
+inline constexpr optional<T> make_optional(std::initializer_list<U> il,
+ Args &&... args) {
+ return optional<T>(in_place, il, std::forward<Args>(args)...);
+}
+
+#if __cplusplus >= 201703L
+template <class T> optional(T)->optional<T>;
+#endif
+
+/// \exclude
+namespace detail {
+#ifdef TL_OPTIONAL_CXX14
+template <class Opt, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Opt>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto optional_map_impl(Opt &&opt, F &&f) {
+ return opt.has_value()
+ ? detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt))
+ : optional<Ret>(nullopt);
+}
+
+template <class Opt, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Opt>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto optional_map_impl(Opt &&opt, F &&f) {
+ if (opt.has_value()) {
+ detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt));
+ return make_optional(monostate{});
+ }
+
+ return optional<monostate>(nullopt);
+}
+#else
+template <class Opt, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Opt>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto optional_map_impl(Opt &&opt, F &&f) -> optional<Ret> {
+ return opt.has_value()
+ ? detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt))
+ : optional<Ret>(nullopt);
+}
+
+template <class Opt, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Opt>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto optional_map_impl(Opt &&opt, F &&f) -> optional<monostate> {
+ if (opt.has_value()) {
+ detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt));
+ return monostate{};
+ }
+
+ return nullopt;
+}
+#endif
+} // namespace detail
+
+/// Specialization for when `T` is a reference. `optional<T&>` acts similarly
+/// to a `T*`, but provides more operations and shows intent more clearly.
+template <class T> class optional<T &> {
+public:
+// The different versions for C++14 and 11 are needed because deduced return
+// types are not SFINAE-safe. This provides better support for things like
+// generic lambdas. C.f.
+// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0826r0.html
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+
+ /// Carries out some operation which returns an optional on the stored
+ /// object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) & {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) && {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F> constexpr auto and_then(F &&f) const & {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F> constexpr auto and_then(F &&f) const && {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+#endif
+#else
+ /// Carries out some operation which returns an optional on the stored
+ /// object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) & {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) && {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F>
+ constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const & {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const && {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation on the stored object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto map(F &&f) const & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto map(F &&f) const && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ /// Carries out some operation on the stored object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional &>(),
+ std::declval<F &&>()))
+ map(F &&f) & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional &&>(),
+ std::declval<F &&>()))
+ map(F &&f) && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F>
+ constexpr decltype(detail::optional_map_impl(std::declval<const optional &>(),
+ std::declval<F &&>()))
+ map(F &&f) const & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr decltype(detail::optional_map_impl(std::declval<const optional &&>(),
+ std::declval<F &&>()))
+ map(F &&f) const && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation on the stored object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto transform(F&& f) const & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto transform(F&& f) const && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ /// Carries out some operation on the stored object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional&>(),
+ std::declval<F&&>()))
+ transform(F&& f) & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ /// \group map
+ /// \synopsis template <class F> auto transform(F &&f) &&;
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional&&>(),
+ std::declval<F&&>()))
+ transform(F&& f) && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F>
+ constexpr decltype(detail::optional_map_impl(std::declval<const optional&>(),
+ std::declval<F&&>()))
+ transform(F&& f) const & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr decltype(detail::optional_map_impl(std::declval<const optional&&>(),
+ std::declval<F&&>()))
+ transform(F&& f) const && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+ /// Calls `f` if the optional is empty
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+ if (has_value())
+ return *this;
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+ return has_value() ? *this : std::forward<F>(f)();
+ }
+
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) && {
+ if (has_value())
+ return std::move(*this);
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) && {
+ return has_value() ? std::move(*this) : std::forward<F>(f)();
+ }
+
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const & {
+ if (has_value())
+ return *this;
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) const & {
+ return has_value() ? *this : std::forward<F>(f)();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const && {
+ if (has_value())
+ return std::move(*this);
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const && {
+ return has_value() ? std::move(*this) : std::forward<F>(f)();
+ }
+#endif
+
+ /// Maps the stored value with `f` if there is one, otherwise returns `u`
+ template <class F, class U> U map_or(F &&f, U &&u) & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u);
+ }
+
+ template <class F, class U> U map_or(F &&f, U &&u) && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u);
+ }
+
+ template <class F, class U> U map_or(F &&f, U &&u) const & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, class U> U map_or(F &&f, U &&u) const && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u);
+ }
+#endif
+
+ /// Maps the stored value with `f` if there is one, otherwise calls
+ /// `u` and returns the result.
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u)();
+ }
+
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u)();
+ }
+
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u)();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u)();
+ }
+#endif
+
+ /// Returns `u` if `*this` has a value, otherwise an empty optional.
+ template <class U>
+ constexpr optional<typename std::decay<U>::type> conjunction(U &&u) const {
+ using result = optional<detail::decay_t<U>>;
+ return has_value() ? result{u} : result{nullopt};
+ }
+
+ /// Returns `rhs` if `*this` is empty, otherwise the current value.
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) & {
+ return has_value() ? *this : rhs;
+ }
+
+ constexpr optional disjunction(const optional &rhs) const & {
+ return has_value() ? *this : rhs;
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) && {
+ return has_value() ? std::move(*this) : rhs;
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr optional disjunction(const optional &rhs) const && {
+ return has_value() ? std::move(*this) : rhs;
+ }
+#endif
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) & {
+ return has_value() ? *this : std::move(rhs);
+ }
+
+ constexpr optional disjunction(optional &&rhs) const & {
+ return has_value() ? *this : std::move(rhs);
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) && {
+ return has_value() ? std::move(*this) : std::move(rhs);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr optional disjunction(optional &&rhs) const && {
+ return has_value() ? std::move(*this) : std::move(rhs);
+ }
+#endif
+
+ /// Takes the value out of the optional, leaving it empty
+ optional take() {
+ optional ret = std::move(*this);
+ reset();
+ return ret;
+ }
+
+ using value_type = T &;
+
+ /// Constructs an optional that does not contain a value.
+ constexpr optional() noexcept : m_value(nullptr) {}
+
+ constexpr optional(nullopt_t) noexcept : m_value(nullptr) {}
+
+ /// Copy constructor
+ ///
+ /// If `rhs` contains a value, the stored value is direct-initialized with
+ /// it. Otherwise, the constructed optional is empty.
+ TL_OPTIONAL_11_CONSTEXPR optional(const optional &rhs) noexcept = default;
+
+ /// Move constructor
+ ///
+ /// If `rhs` contains a value, the stored value is direct-initialized with
+ /// it. Otherwise, the constructed optional is empty.
+ TL_OPTIONAL_11_CONSTEXPR optional(optional &&rhs) = default;
+
+ /// Constructs the stored value with `u`.
+ template <class U = T,
+ detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+ * = nullptr>
+ constexpr optional(U &&u) noexcept : m_value(std::addressof(u)) {
+ static_assert(std::is_lvalue_reference<U>::value, "U must be an lvalue");
+ }
+
+ template <class U>
+ constexpr explicit optional(const optional<U> &rhs) noexcept : optional(*rhs) {}
+
+ /// No-op
+ ~optional() = default;
+
+ /// Assignment to empty.
+ ///
+ /// Destroys the current value if there is one.
+ optional &operator=(nullopt_t) noexcept {
+ m_value = nullptr;
+ return *this;
+ }
+
+ /// Copy assignment.
+ ///
+ /// Rebinds this optional to the referee of `rhs` if there is one. Otherwise
+ /// resets the stored value in `*this`.
+ optional &operator=(const optional &rhs) = default;
+
+ /// Rebinds this optional to `u`.
+ template <class U = T,
+ detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+ * = nullptr>
+ optional &operator=(U &&u) {
+ static_assert(std::is_lvalue_reference<U>::value, "U must be an lvalue");
+ m_value = std::addressof(u);
+ return *this;
+ }
+
+ /// Converting copy assignment operator.
+ ///
+ /// Rebinds this optional to the referee of `rhs` if there is one. Otherwise
+ /// resets the stored value in `*this`.
+ template <class U> optional &operator=(const optional<U> &rhs) noexcept {
+ m_value = std::addressof(rhs.value());
+ return *this;
+ }
+
+ /// Rebinds this optional to `u`.
+ template <class U = T,
+ detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+ * = nullptr>
+ optional &emplace(U &&u) noexcept {
+ return *this = std::forward<U>(u);
+ }
+
+ void swap(optional &rhs) noexcept { std::swap(m_value, rhs.m_value); }
+
+ /// Returns a pointer to the stored value
+ constexpr const T *operator->() const noexcept { return m_value; }
+
+ TL_OPTIONAL_11_CONSTEXPR T *operator->() noexcept { return m_value; }
+
+ /// Returns the stored value
+ TL_OPTIONAL_11_CONSTEXPR T &operator*() noexcept { return *m_value; }
+
+ constexpr const T &operator*() const noexcept { return *m_value; }
+
+ constexpr bool has_value() const noexcept { return m_value != nullptr; }
+
+ constexpr explicit operator bool() const noexcept {
+ return m_value != nullptr;
+ }
+
+ /// Returns the contained value if there is one, otherwise throws bad_optional_access
+ TL_OPTIONAL_11_CONSTEXPR T &value() {
+ if (has_value())
+ return *m_value;
+ throw bad_optional_access();
+ }
+ TL_OPTIONAL_11_CONSTEXPR const T &value() const {
+ if (has_value())
+ return *m_value;
+ throw bad_optional_access();
+ }
+
+ /// Returns the stored value if there is one, otherwise returns `u`
+ template <class U> constexpr T value_or(U &&u) const & noexcept {
+ static_assert(std::is_copy_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be copy constructible and convertible from U");
+ return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+ }
+
+ /// \group value_or
+ template <class U> TL_OPTIONAL_11_CONSTEXPR T value_or(U &&u) && noexcept {
+ static_assert(std::is_move_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be move constructible and convertible from U");
+ return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+ }
+
+ /// Destroys the stored value if one exists, making the optional empty
+ void reset() noexcept { m_value = nullptr; }
+
+private:
+ T *m_value;
+}; // namespace tl
+
+
+
+} // namespace tl
+
+namespace std {
+// TODO SFINAE
+template <class T> struct hash<tl::optional<T>> {
+ ::std::size_t operator()(const tl::optional<T> &o) const {
+ if (!o.has_value())
+ return 0;
+
+ return std::hash<tl::detail::remove_const_t<T>>()(*o);
+ }
+};
+} // namespace std
+
+#endif
--- /dev/null
+// Copyright 2016-2018 by Martin Moene
+//
+// https://github.com/martinmoene/variant-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_VARIANT_LITE_HPP
+#define NONSTD_VARIANT_LITE_HPP
+
+#define variant_lite_MAJOR 1
+#define variant_lite_MINOR 2
+#define variant_lite_PATCH 2
+
+#define variant_lite_VERSION \
+ variant_STRINGIFY(variant_lite_MAJOR) "." variant_STRINGIFY( \
+ variant_lite_MINOR) "." variant_STRINGIFY(variant_lite_PATCH)
+
+#define variant_STRINGIFY(x) variant_STRINGIFY_(x)
+#define variant_STRINGIFY_(x) #x
+
+// variant-lite configuration:
+
+#define variant_VARIANT_DEFAULT 0
+#define variant_VARIANT_NONSTD 1
+#define variant_VARIANT_STD 2
+
+#if !defined(variant_CONFIG_SELECT_VARIANT)
+#define variant_CONFIG_SELECT_VARIANT \
+ (variant_HAVE_STD_VARIANT ? variant_VARIANT_STD : variant_VARIANT_NONSTD)
+#endif
+
+#ifndef variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO 0
+#endif
+
+#ifndef variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO 0
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef variant_CONFIG_NO_EXCEPTIONS
+#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define variant_CONFIG_NO_EXCEPTIONS 0
+#else
+#define variant_CONFIG_NO_EXCEPTIONS 1
+#endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef variant_CPLUSPLUS
+#if defined(_MSVC_LANG) && !defined(__clang__)
+#define variant_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
+#else
+#define variant_CPLUSPLUS __cplusplus
+#endif
+#endif
+
+#define variant_CPP98_OR_GREATER (variant_CPLUSPLUS >= 199711L)
+#define variant_CPP11_OR_GREATER (variant_CPLUSPLUS >= 201103L)
+#define variant_CPP11_OR_GREATER_ (variant_CPLUSPLUS >= 201103L)
+#define variant_CPP14_OR_GREATER (variant_CPLUSPLUS >= 201402L)
+#define variant_CPP17_OR_GREATER (variant_CPLUSPLUS >= 201703L)
+#define variant_CPP20_OR_GREATER (variant_CPLUSPLUS >= 202000L)
+
+// Use C++17 std::variant if available and requested:
+
+#if variant_CPP17_OR_GREATER && defined(__has_include)
+#if __has_include(<variant> )
+#define variant_HAVE_STD_VARIANT 1
+#else
+#define variant_HAVE_STD_VARIANT 0
+#endif
+#else
+#define variant_HAVE_STD_VARIANT 0
+#endif
+
+#define variant_USES_STD_VARIANT \
+ ((variant_CONFIG_SELECT_VARIANT == variant_VARIANT_STD) || \
+ ((variant_CONFIG_SELECT_VARIANT == variant_VARIANT_DEFAULT) && variant_HAVE_STD_VARIANT))
+
+//
+// in_place: code duplicated in any-lite, expected-lite, optional-lite, value-ptr-lite,
+// variant-lite:
+//
+
+#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES
+#define nonstd_lite_HAVE_IN_PLACE_TYPES 1
+
+// C++17 std::in_place in <utility>:
+
+#if variant_CPP17_OR_GREATER
+
+#include <utility>
+
+namespace nonstd {
+
+using std::in_place;
+using std::in_place_index;
+using std::in_place_index_t;
+using std::in_place_t;
+using std::in_place_type;
+using std::in_place_type_t;
+
+#define nonstd_lite_in_place_t(T) std::in_place_t
+#define nonstd_lite_in_place_type_t(T) std::in_place_type_t<T>
+#define nonstd_lite_in_place_index_t(K) std::in_place_index_t<K>
+
+#define nonstd_lite_in_place(T) \
+ std::in_place_t {}
+#define nonstd_lite_in_place_type(T) \
+ std::in_place_type_t<T> {}
+#define nonstd_lite_in_place_index(K) \
+ std::in_place_index_t<K> {}
+
+} // namespace nonstd
+
+#else // variant_CPP17_OR_GREATER
+
+#include <cstddef>
+
+namespace nonstd {
+namespace detail {
+
+template <class T> struct in_place_type_tag {};
+
+template <std::size_t K> struct in_place_index_tag {};
+
+} // namespace detail
+
+struct in_place_t {};
+
+template <class T>
+inline in_place_t in_place(detail::in_place_type_tag<T> = detail::in_place_type_tag<T>()) {
+ return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t in_place(detail::in_place_index_tag<K> = detail::in_place_index_tag<K>()) {
+ return in_place_t();
+}
+
+template <class T>
+inline in_place_t in_place_type(detail::in_place_type_tag<T> = detail::in_place_type_tag<T>()) {
+ return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t in_place_index(detail::in_place_index_tag<K> = detail::in_place_index_tag<K>()) {
+ return in_place_t();
+}
+
+// mimic templated typedef:
+
+#define nonstd_lite_in_place_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_type_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_index_t(K) \
+ nonstd::in_place_t (&)(nonstd::detail::in_place_index_tag<K>)
+
+#define nonstd_lite_in_place(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_type(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_index(K) nonstd::in_place_index<K>
+
+} // namespace nonstd
+
+#endif // variant_CPP17_OR_GREATER
+#endif // nonstd_lite_HAVE_IN_PLACE_TYPES
+
+//
+// Use C++17 std::variant:
+//
+
+#if variant_USES_STD_VARIANT
+
+#include <functional> // std::hash<>
+#include <variant>
+
+#if !variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_size_V(T) nonstd::variant_size<T>::value
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_alternative_T(K, T) typename nonstd::variant_alternative<K, T>::type
+#endif
+
+namespace nonstd {
+
+using std::bad_variant_access;
+using std::hash;
+using std::monostate;
+using std::variant;
+using std::variant_alternative;
+using std::variant_alternative_t;
+using std::variant_size;
+using std::variant_size_v;
+
+using std::get;
+using std::get_if;
+using std::holds_alternative;
+using std::visit;
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+using std::swap;
+
+constexpr auto variant_npos = std::variant_npos;
+} // namespace nonstd
+
+#else // variant_USES_STD_VARIANT
+
+#include <cstddef>
+#include <limits>
+#include <new>
+#include <utility>
+
+#if variant_CONFIG_NO_EXCEPTIONS
+#include <cassert>
+#else
+#include <stdexcept>
+#endif
+
+// variant-lite type and visitor argument count configuration (script/generate_header.py):
+
+#define variant_CONFIG_MAX_TYPE_COUNT 16
+#define variant_CONFIG_MAX_VISITOR_ARG_COUNT 5
+
+// variant-lite alignment configuration:
+
+#ifndef variant_CONFIG_MAX_ALIGN_HACK
+#define variant_CONFIG_MAX_ALIGN_HACK 0
+#endif
+
+#ifndef variant_CONFIG_ALIGN_AS
+// no default, used in #if defined()
+#endif
+
+#ifndef variant_CONFIG_ALIGN_AS_FALLBACK
+#define variant_CONFIG_ALIGN_AS_FALLBACK double
+#endif
+
+// half-open range [lo..hi):
+#define variant_BETWEEN(v, lo, hi) ((lo) <= (v) && (v) < (hi))
+
+// Compiler versions:
+//
+// MSVC++ 6.0 _MSC_VER == 1200 variant_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0)
+// MSVC++ 7.0 _MSC_VER == 1300 variant_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002)
+// MSVC++ 7.1 _MSC_VER == 1310 variant_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003)
+// MSVC++ 8.0 _MSC_VER == 1400 variant_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005)
+// MSVC++ 9.0 _MSC_VER == 1500 variant_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008)
+// MSVC++ 10.0 _MSC_VER == 1600 variant_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010)
+// MSVC++ 11.0 _MSC_VER == 1700 variant_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012)
+// MSVC++ 12.0 _MSC_VER == 1800 variant_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013)
+// MSVC++ 14.0 _MSC_VER == 1900 variant_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015)
+// MSVC++ 14.1 _MSC_VER >= 1910 variant_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017)
+// MSVC++ 14.2 _MSC_VER >= 1920 variant_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define variant_COMPILER_MSVC_VER (_MSC_VER)
+#define variant_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * (5 + (_MSC_VER < 1900)))
+#else
+#define variant_COMPILER_MSVC_VER 0
+#define variant_COMPILER_MSVC_VERSION 0
+#endif
+
+#define variant_COMPILER_VERSION(major, minor, patch) (10 * (10 * (major) + (minor)) + (patch))
+
+#if defined(__clang__)
+#define variant_COMPILER_CLANG_VERSION \
+ variant_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+#define variant_COMPILER_CLANG_VERSION 0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__)
+#define variant_COMPILER_GNUC_VERSION \
+ variant_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+#define variant_COMPILER_GNUC_VERSION 0
+#endif
+
+#if variant_BETWEEN(variant_COMPILER_MSVC_VER, 1300, 1900)
+#pragma warning(push)
+#pragma warning(disable : 4345) // initialization behavior changed
+#endif
+
+// Presence of language and library features:
+
+#define variant_HAVE(feature) (variant_HAVE_##feature)
+
+#ifdef _HAS_CPP0X
+#define variant_HAS_CPP0X _HAS_CPP0X
+#else
+#define variant_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for variant-lite:
+
+#if variant_COMPILER_MSVC_VER >= 1900
+#undef variant_CPP11_OR_GREATER
+#define variant_CPP11_OR_GREATER 1
+#endif
+
+#define variant_CPP11_90 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1500)
+#define variant_CPP11_100 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1600)
+#define variant_CPP11_110 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1700)
+#define variant_CPP11_120 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1800)
+#define variant_CPP11_140 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1900)
+#define variant_CPP11_141 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1910)
+
+#define variant_CPP14_000 (variant_CPP14_OR_GREATER)
+#define variant_CPP17_000 (variant_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define variant_HAVE_CONSTEXPR_11 variant_CPP11_140
+#define variant_HAVE_INITIALIZER_LIST variant_CPP11_120
+#define variant_HAVE_NOEXCEPT variant_CPP11_140
+#define variant_HAVE_NULLPTR variant_CPP11_100
+#define variant_HAVE_OVERRIDE variant_CPP11_140
+
+// Presence of C++14 language features:
+
+#define variant_HAVE_CONSTEXPR_14 variant_CPP14_000
+
+// Presence of C++17 language features:
+
+// no flag
+
+// Presence of C++ library features:
+
+#define variant_HAVE_CONDITIONAL variant_CPP11_120
+#define variant_HAVE_REMOVE_CV variant_CPP11_120
+#define variant_HAVE_STD_ADD_POINTER variant_CPP11_90
+#define variant_HAVE_TYPE_TRAITS variant_CPP11_90
+
+#define variant_HAVE_TR1_TYPE_TRAITS (!!variant_COMPILER_GNUC_VERSION)
+#define variant_HAVE_TR1_ADD_POINTER (!!variant_COMPILER_GNUC_VERSION)
+
+// C++ feature usage:
+
+#if variant_HAVE_CONSTEXPR_11
+#define variant_constexpr constexpr
+#else
+#define variant_constexpr /*constexpr*/
+#endif
+
+#if variant_HAVE_CONSTEXPR_14
+#define variant_constexpr14 constexpr
+#else
+#define variant_constexpr14 /*constexpr*/
+#endif
+
+#if variant_HAVE_NOEXCEPT
+#define variant_noexcept noexcept
+#else
+#define variant_noexcept /*noexcept*/
+#endif
+
+#if variant_HAVE_NULLPTR
+#define variant_nullptr nullptr
+#else
+#define variant_nullptr NULL
+#endif
+
+#if variant_HAVE_OVERRIDE
+#define variant_override override
+#else
+#define variant_override /*override*/
+#endif
+
+// additional includes:
+
+#if variant_CPP11_OR_GREATER
+#include <functional> // std::hash
+#endif
+
+#if variant_HAVE_INITIALIZER_LIST
+#include <initializer_list>
+#endif
+
+#if variant_HAVE_TYPE_TRAITS
+#include <type_traits>
+#elif variant_HAVE_TR1_TYPE_TRAITS
+#include <tr1/type_traits>
+#endif
+
+// Method enabling
+
+#if variant_CPP11_OR_GREATER
+
+#define variant_REQUIRES_0(...) \
+ template <bool B = (__VA_ARGS__), typename std::enable_if<B, int>::type = 0>
+
+#define variant_REQUIRES_T(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0
+
+#define variant_REQUIRES_R(R, ...) typename std::enable_if<(__VA_ARGS__), R>::type
+
+#define variant_REQUIRES_A(...) , typename std::enable_if<(__VA_ARGS__), void *>::type = nullptr
+
+#endif
+
+//
+// variant:
+//
+
+namespace nonstd {
+namespace variants {
+
+// C++11 emulation:
+
+namespace std11 {
+
+#if variant_HAVE_STD_ADD_POINTER
+
+using std::add_pointer;
+
+#elif variant_HAVE_TR1_ADD_POINTER
+
+using std::tr1::add_pointer;
+
+#else
+
+template <class T> struct remove_reference { typedef T type; };
+template <class T> struct remove_reference<T &> { typedef T type; };
+
+template <class T> struct add_pointer { typedef typename remove_reference<T>::type *type; };
+
+#endif // variant_HAVE_STD_ADD_POINTER
+
+#if variant_HAVE_REMOVE_CV
+
+using std::remove_cv;
+
+#else
+
+template <class T> struct remove_const { typedef T type; };
+template <class T> struct remove_const<const T> { typedef T type; };
+
+template <class T> struct remove_volatile { typedef T type; };
+template <class T> struct remove_volatile<volatile T> { typedef T type; };
+
+template <class T> struct remove_cv {
+ typedef typename remove_volatile<typename remove_const<T>::type>::type type;
+};
+
+#endif // variant_HAVE_REMOVE_CV
+
+#if variant_HAVE_CONDITIONAL
+
+using std::conditional;
+
+#else
+
+template <bool Cond, class Then, class Else> struct conditional;
+
+template <class Then, class Else> struct conditional<true, Then, Else> { typedef Then type; };
+
+template <class Then, class Else> struct conditional<false, Then, Else> { typedef Else type; };
+
+#endif // variant_HAVE_CONDITIONAL
+
+} // namespace std11
+
+/// type traits C++17:
+
+namespace std17 {
+
+#if variant_CPP17_OR_GREATER
+
+using std::is_nothrow_swappable;
+using std::is_swappable;
+
+#elif variant_CPP11_OR_GREATER
+
+namespace detail {
+
+using std::swap;
+
+struct is_swappable {
+ template <typename T, typename = decltype(swap(std::declval<T &>(), std::declval<T &>()))>
+ static std::true_type test(int);
+
+ template <typename> static std::false_type test(...);
+};
+
+struct is_nothrow_swappable {
+ // wrap noexcept(epr) in separate function as work-around for VC140 (VS2015):
+
+ template <typename T> static constexpr bool test() {
+ return noexcept(swap(std::declval<T &>(), std::declval<T &>()));
+ }
+
+ template <typename T> static auto test(int) -> std::integral_constant<bool, test<T>()> {}
+
+ template <typename> static std::false_type test(...);
+};
+
+} // namespace detail
+
+// is [nothow] swappable:
+
+template <typename T> struct is_swappable : decltype(detail::is_swappable::test<T>(0)) {};
+
+template <typename T>
+struct is_nothrow_swappable : decltype(detail::is_nothrow_swappable::test<T>(0)) {};
+
+#endif // variant_CPP17_OR_GREATER
+
+} // namespace std17
+
+// detail:
+
+namespace detail {
+
+// typelist:
+
+#define variant_TL1(T1) detail::typelist<T1, detail::nulltype>
+#define variant_TL2(T1, T2) detail::typelist<T1, variant_TL1(T2)>
+#define variant_TL3(T1, T2, T3) detail::typelist<T1, variant_TL2(T2, T3)>
+#define variant_TL4(T1, T2, T3, T4) detail::typelist<T1, variant_TL3(T2, T3, T4)>
+#define variant_TL5(T1, T2, T3, T4, T5) detail::typelist<T1, variant_TL4(T2, T3, T4, T5)>
+#define variant_TL6(T1, T2, T3, T4, T5, T6) detail::typelist<T1, variant_TL5(T2, T3, T4, T5, T6)>
+#define variant_TL7(T1, T2, T3, T4, T5, T6, T7) \
+ detail::typelist<T1, variant_TL6(T2, T3, T4, T5, T6, T7)>
+#define variant_TL8(T1, T2, T3, T4, T5, T6, T7, T8) \
+ detail::typelist<T1, variant_TL7(T2, T3, T4, T5, T6, T7, T8)>
+#define variant_TL9(T1, T2, T3, T4, T5, T6, T7, T8, T9) \
+ detail::typelist<T1, variant_TL8(T2, T3, T4, T5, T6, T7, T8, T9)>
+#define variant_TL10(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) \
+ detail::typelist<T1, variant_TL9(T2, T3, T4, T5, T6, T7, T8, T9, T10)>
+#define variant_TL11(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) \
+ detail::typelist<T1, variant_TL10(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)>
+#define variant_TL12(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) \
+ detail::typelist<T1, variant_TL11(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)>
+#define variant_TL13(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) \
+ detail::typelist<T1, variant_TL12(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)>
+#define variant_TL14(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) \
+ detail::typelist<T1, variant_TL13(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)>
+#define variant_TL15(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) \
+ detail::typelist<T1, variant_TL14(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)>
+#define variant_TL16(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16) \
+ detail::typelist<T1, variant_TL15(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, \
+ T16)>
+
+// variant parameter unused type tags:
+
+template <class T> struct TX : T {
+ inline TX<T> operator+() const { return TX<T>(); }
+ inline TX<T> operator-() const { return TX<T>(); }
+
+ inline TX<T> operator!() const { return TX<T>(); }
+ inline TX<T> operator~() const { return TX<T>(); }
+
+ inline TX<T> *operator&() const { return variant_nullptr; }
+
+ template <class U> inline TX<T> operator*(U const &)const { return TX<T>(); }
+ template <class U> inline TX<T> operator/(U const &) const { return TX<T>(); }
+
+ template <class U> inline TX<T> operator%(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator+(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator-(U const &) const { return TX<T>(); }
+
+ template <class U> inline TX<T> operator<<(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator>>(U const &) const { return TX<T>(); }
+
+ inline bool operator==(T const &) const { return false; }
+ inline bool operator<(T const &) const { return false; }
+
+ template <class U> inline TX<T> operator&(U const &)const { return TX<T>(); }
+ template <class U> inline TX<T> operator|(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator^(U const &) const { return TX<T>(); }
+
+ template <class U> inline TX<T> operator&&(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator||(U const &) const { return TX<T>(); }
+};
+
+struct S0 {};
+typedef TX<S0> T0;
+struct S1 {};
+typedef TX<S1> T1;
+struct S2 {};
+typedef TX<S2> T2;
+struct S3 {};
+typedef TX<S3> T3;
+struct S4 {};
+typedef TX<S4> T4;
+struct S5 {};
+typedef TX<S5> T5;
+struct S6 {};
+typedef TX<S6> T6;
+struct S7 {};
+typedef TX<S7> T7;
+struct S8 {};
+typedef TX<S8> T8;
+struct S9 {};
+typedef TX<S9> T9;
+struct S10 {};
+typedef TX<S10> T10;
+struct S11 {};
+typedef TX<S11> T11;
+struct S12 {};
+typedef TX<S12> T12;
+struct S13 {};
+typedef TX<S13> T13;
+struct S14 {};
+typedef TX<S14> T14;
+struct S15 {};
+typedef TX<S15> T15;
+
+struct nulltype {};
+
+template <class Head, class Tail> struct typelist {
+ typedef Head head;
+ typedef Tail tail;
+};
+
+// typelist max element size:
+
+template <class List> struct typelist_max;
+
+template <> struct typelist_max<nulltype> {
+ enum V { value = 0 };
+ typedef void type;
+};
+
+template <class Head, class Tail> struct typelist_max<typelist<Head, Tail>> {
+private:
+ enum TV { tail_value = size_t(typelist_max<Tail>::value) };
+
+ typedef typename typelist_max<Tail>::type tail_type;
+
+public:
+ enum V { value = (sizeof(Head) > tail_value) ? sizeof(Head) : std::size_t(tail_value) };
+
+ typedef typename std11::conditional<(sizeof(Head) > tail_value), Head, tail_type>::type type;
+};
+
+#if variant_CPP11_OR_GREATER
+
+// typelist max alignof element type:
+
+template <class List> struct typelist_max_alignof;
+
+template <> struct typelist_max_alignof<nulltype> {
+ enum V { value = 0 };
+};
+
+template <class Head, class Tail> struct typelist_max_alignof<typelist<Head, Tail>> {
+private:
+ enum TV { tail_value = size_t(typelist_max_alignof<Tail>::value) };
+
+public:
+ enum V { value = (alignof(Head) > tail_value) ? alignof(Head) : std::size_t(tail_value) };
+};
+
+#endif
+
+// typelist size (length):
+
+template <class List> struct typelist_size {
+ enum V { value = 1 };
+};
+
+template <> struct typelist_size<T0> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T1> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T2> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T3> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T4> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T5> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T6> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T7> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T8> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T9> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T10> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T11> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T12> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T13> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T14> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T15> {
+ enum V { value = 0 };
+};
+
+template <> struct typelist_size<nulltype> {
+ enum V { value = 0 };
+};
+
+template <class Head, class Tail> struct typelist_size<typelist<Head, Tail>> {
+ enum V { value = typelist_size<Head>::value + typelist_size<Tail>::value };
+};
+
+// typelist index of type:
+
+template <class List, class T> struct typelist_index_of;
+
+template <class T> struct typelist_index_of<nulltype, T> {
+ enum V { value = -1 };
+};
+
+template <class Tail, class T> struct typelist_index_of<typelist<T, Tail>, T> {
+ enum V { value = 0 };
+};
+
+template <class Head, class Tail, class T> struct typelist_index_of<typelist<Head, Tail>, T> {
+private:
+ enum TV { nextVal = typelist_index_of<Tail, T>::value };
+
+public:
+ enum V { value = nextVal == -1 ? -1 : 1 + nextVal };
+};
+
+// typelist type at index:
+
+template <class List, std::size_t i> struct typelist_type_at;
+
+template <class Head, class Tail> struct typelist_type_at<typelist<Head, Tail>, 0> {
+ typedef Head type;
+};
+
+template <class Head, class Tail, std::size_t i> struct typelist_type_at<typelist<Head, Tail>, i> {
+ typedef typename typelist_type_at<Tail, i - 1>::type type;
+};
+
+#if variant_CONFIG_MAX_ALIGN_HACK
+
+// Max align, use most restricted type for alignment:
+
+#define variant_UNIQUE(name) variant_UNIQUE2(name, __LINE__)
+#define variant_UNIQUE2(name, line) variant_UNIQUE3(name, line)
+#define variant_UNIQUE3(name, line) name##line
+
+#define variant_ALIGN_TYPE(type) \
+ type variant_UNIQUE(_t); \
+ struct_t<type> variant_UNIQUE(_st)
+
+template <class T> struct struct_t { T _; };
+
+union max_align_t {
+ variant_ALIGN_TYPE(char);
+ variant_ALIGN_TYPE(short int);
+ variant_ALIGN_TYPE(int);
+ variant_ALIGN_TYPE(long int);
+ variant_ALIGN_TYPE(float);
+ variant_ALIGN_TYPE(double);
+ variant_ALIGN_TYPE(long double);
+ variant_ALIGN_TYPE(char *);
+ variant_ALIGN_TYPE(short int *);
+ variant_ALIGN_TYPE(int *);
+ variant_ALIGN_TYPE(long int *);
+ variant_ALIGN_TYPE(float *);
+ variant_ALIGN_TYPE(double *);
+ variant_ALIGN_TYPE(long double *);
+ variant_ALIGN_TYPE(void *);
+
+#ifdef HAVE_LONG_LONG
+ variant_ALIGN_TYPE(long long);
+#endif
+
+ struct Unknown;
+
+ Unknown (*variant_UNIQUE(_))(Unknown);
+ Unknown *Unknown::*variant_UNIQUE(_);
+ Unknown (Unknown::*variant_UNIQUE(_))(Unknown);
+
+ struct_t<Unknown (*)(Unknown)> variant_UNIQUE(_);
+ struct_t<Unknown * Unknown::*> variant_UNIQUE(_);
+ struct_t<Unknown (Unknown::*)(Unknown)> variant_UNIQUE(_);
+};
+
+#undef variant_UNIQUE
+#undef variant_UNIQUE2
+#undef variant_UNIQUE3
+
+#undef variant_ALIGN_TYPE
+
+#elif defined(variant_CONFIG_ALIGN_AS) // variant_CONFIG_MAX_ALIGN_HACK
+
+// Use user-specified type for alignment:
+
+#define variant_ALIGN_AS(unused) variant_CONFIG_ALIGN_AS
+
+#else // variant_CONFIG_MAX_ALIGN_HACK
+
+// Determine POD type to use for alignment:
+
+#define variant_ALIGN_AS(to_align) \
+ typename detail::type_of_size<detail::alignment_types, \
+ detail::alignment_of<to_align>::value>::type
+
+template <typename T> struct alignment_of;
+
+template <typename T> struct alignment_of_hack {
+ char c;
+ T t;
+ alignment_of_hack();
+};
+
+template <size_t A, size_t S> struct alignment_logic {
+ enum V { value = A < S ? A : S };
+};
+
+template <typename T> struct alignment_of {
+ enum V { value = alignment_logic<sizeof(alignment_of_hack<T>) - sizeof(T), sizeof(T)>::value };
+};
+
+template <typename List, size_t N> struct type_of_size {
+ typedef
+ typename std11::conditional<N == sizeof(typename List::head), typename List::head,
+ typename type_of_size<typename List::tail, N>::type>::type type;
+};
+
+template <size_t N> struct type_of_size<nulltype, N> {
+ typedef variant_CONFIG_ALIGN_AS_FALLBACK type;
+};
+
+template <typename T> struct struct_t { T _; };
+
+#define variant_ALIGN_TYPE(type) typelist < type, typelist < struct_t<type>
+
+struct Unknown;
+
+typedef variant_ALIGN_TYPE(char), variant_ALIGN_TYPE(short), variant_ALIGN_TYPE(int),
+ variant_ALIGN_TYPE(long), variant_ALIGN_TYPE(float), variant_ALIGN_TYPE(double),
+ variant_ALIGN_TYPE(long double),
+
+ variant_ALIGN_TYPE(char *), variant_ALIGN_TYPE(short *), variant_ALIGN_TYPE(int *),
+ variant_ALIGN_TYPE(long *), variant_ALIGN_TYPE(float *), variant_ALIGN_TYPE(double *),
+ variant_ALIGN_TYPE(long double *),
+
+ variant_ALIGN_TYPE(Unknown (*)(Unknown)), variant_ALIGN_TYPE(Unknown *Unknown::*),
+ variant_ALIGN_TYPE(Unknown (Unknown::*)(Unknown)),
+
+ nulltype >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> alignment_types;
+
+#undef variant_ALIGN_TYPE
+
+#endif // variant_CONFIG_MAX_ALIGN_HACK
+
+#if variant_CPP11_OR_GREATER
+
+template <typename T> inline std::size_t hash(T const &v) { return std::hash<T>()(v); }
+
+inline std::size_t hash(T0 const &) { return 0; }
+inline std::size_t hash(T1 const &) { return 0; }
+inline std::size_t hash(T2 const &) { return 0; }
+inline std::size_t hash(T3 const &) { return 0; }
+inline std::size_t hash(T4 const &) { return 0; }
+inline std::size_t hash(T5 const &) { return 0; }
+inline std::size_t hash(T6 const &) { return 0; }
+inline std::size_t hash(T7 const &) { return 0; }
+inline std::size_t hash(T8 const &) { return 0; }
+inline std::size_t hash(T9 const &) { return 0; }
+inline std::size_t hash(T10 const &) { return 0; }
+inline std::size_t hash(T11 const &) { return 0; }
+inline std::size_t hash(T12 const &) { return 0; }
+inline std::size_t hash(T13 const &) { return 0; }
+inline std::size_t hash(T14 const &) { return 0; }
+inline std::size_t hash(T15 const &) { return 0; }
+
+#endif // variant_CPP11_OR_GREATER
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct helper {
+ typedef signed char type_index_t;
+ typedef variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15) variant_types;
+
+ template <class U> static U *as(void *data) { return reinterpret_cast<U *>(data); }
+
+ template <class U> static U const *as(void const *data) {
+ return reinterpret_cast<const U *>(data);
+ }
+
+ static type_index_t to_index_t(std::size_t index) { return static_cast<type_index_t>(index); }
+
+ static void destroy(type_index_t index, void *data) {
+ switch (index) {
+ case 0:
+ as<T0>(data)->~T0();
+ break;
+ case 1:
+ as<T1>(data)->~T1();
+ break;
+ case 2:
+ as<T2>(data)->~T2();
+ break;
+ case 3:
+ as<T3>(data)->~T3();
+ break;
+ case 4:
+ as<T4>(data)->~T4();
+ break;
+ case 5:
+ as<T5>(data)->~T5();
+ break;
+ case 6:
+ as<T6>(data)->~T6();
+ break;
+ case 7:
+ as<T7>(data)->~T7();
+ break;
+ case 8:
+ as<T8>(data)->~T8();
+ break;
+ case 9:
+ as<T9>(data)->~T9();
+ break;
+ case 10:
+ as<T10>(data)->~T10();
+ break;
+ case 11:
+ as<T11>(data)->~T11();
+ break;
+ case 12:
+ as<T12>(data)->~T12();
+ break;
+ case 13:
+ as<T13>(data)->~T13();
+ break;
+ case 14:
+ as<T14>(data)->~T14();
+ break;
+ case 15:
+ as<T15>(data)->~T15();
+ break;
+ }
+ }
+
+#if variant_CPP11_OR_GREATER
+ template <class T, class... Args> static type_index_t construct_t(void *data, Args &&... args) {
+ new (data) T(std::forward<Args>(args)...);
+
+ return to_index_t(detail::typelist_index_of<variant_types, T>::value);
+ }
+
+ template <std::size_t K, class... Args>
+ static type_index_t construct_i(void *data, Args &&... args) {
+ using type = typename detail::typelist_type_at<variant_types, K>::type;
+
+ construct_t<type>(data, std::forward<Args>(args)...);
+
+ return to_index_t(K);
+ }
+
+ static type_index_t move_construct(type_index_t const from_index, void *from_value,
+ void *to_value) {
+ switch (from_index) {
+ case 0:
+ new (to_value) T0(std::move(*as<T0>(from_value)));
+ break;
+ case 1:
+ new (to_value) T1(std::move(*as<T1>(from_value)));
+ break;
+ case 2:
+ new (to_value) T2(std::move(*as<T2>(from_value)));
+ break;
+ case 3:
+ new (to_value) T3(std::move(*as<T3>(from_value)));
+ break;
+ case 4:
+ new (to_value) T4(std::move(*as<T4>(from_value)));
+ break;
+ case 5:
+ new (to_value) T5(std::move(*as<T5>(from_value)));
+ break;
+ case 6:
+ new (to_value) T6(std::move(*as<T6>(from_value)));
+ break;
+ case 7:
+ new (to_value) T7(std::move(*as<T7>(from_value)));
+ break;
+ case 8:
+ new (to_value) T8(std::move(*as<T8>(from_value)));
+ break;
+ case 9:
+ new (to_value) T9(std::move(*as<T9>(from_value)));
+ break;
+ case 10:
+ new (to_value) T10(std::move(*as<T10>(from_value)));
+ break;
+ case 11:
+ new (to_value) T11(std::move(*as<T11>(from_value)));
+ break;
+ case 12:
+ new (to_value) T12(std::move(*as<T12>(from_value)));
+ break;
+ case 13:
+ new (to_value) T13(std::move(*as<T13>(from_value)));
+ break;
+ case 14:
+ new (to_value) T14(std::move(*as<T14>(from_value)));
+ break;
+ case 15:
+ new (to_value) T15(std::move(*as<T15>(from_value)));
+ break;
+ }
+ return from_index;
+ }
+
+ static type_index_t move_assign(type_index_t const from_index, void *from_value, void *to_value) {
+ switch (from_index) {
+ case 0:
+ *as<T0>(to_value) = std::move(*as<T0>(from_value));
+ break;
+ case 1:
+ *as<T1>(to_value) = std::move(*as<T1>(from_value));
+ break;
+ case 2:
+ *as<T2>(to_value) = std::move(*as<T2>(from_value));
+ break;
+ case 3:
+ *as<T3>(to_value) = std::move(*as<T3>(from_value));
+ break;
+ case 4:
+ *as<T4>(to_value) = std::move(*as<T4>(from_value));
+ break;
+ case 5:
+ *as<T5>(to_value) = std::move(*as<T5>(from_value));
+ break;
+ case 6:
+ *as<T6>(to_value) = std::move(*as<T6>(from_value));
+ break;
+ case 7:
+ *as<T7>(to_value) = std::move(*as<T7>(from_value));
+ break;
+ case 8:
+ *as<T8>(to_value) = std::move(*as<T8>(from_value));
+ break;
+ case 9:
+ *as<T9>(to_value) = std::move(*as<T9>(from_value));
+ break;
+ case 10:
+ *as<T10>(to_value) = std::move(*as<T10>(from_value));
+ break;
+ case 11:
+ *as<T11>(to_value) = std::move(*as<T11>(from_value));
+ break;
+ case 12:
+ *as<T12>(to_value) = std::move(*as<T12>(from_value));
+ break;
+ case 13:
+ *as<T13>(to_value) = std::move(*as<T13>(from_value));
+ break;
+ case 14:
+ *as<T14>(to_value) = std::move(*as<T14>(from_value));
+ break;
+ case 15:
+ *as<T15>(to_value) = std::move(*as<T15>(from_value));
+ break;
+ }
+ return from_index;
+ }
+#endif
+
+ static type_index_t copy_construct(type_index_t const from_index, const void *from_value,
+ void *to_value) {
+ switch (from_index) {
+ case 0:
+ new (to_value) T0(*as<T0>(from_value));
+ break;
+ case 1:
+ new (to_value) T1(*as<T1>(from_value));
+ break;
+ case 2:
+ new (to_value) T2(*as<T2>(from_value));
+ break;
+ case 3:
+ new (to_value) T3(*as<T3>(from_value));
+ break;
+ case 4:
+ new (to_value) T4(*as<T4>(from_value));
+ break;
+ case 5:
+ new (to_value) T5(*as<T5>(from_value));
+ break;
+ case 6:
+ new (to_value) T6(*as<T6>(from_value));
+ break;
+ case 7:
+ new (to_value) T7(*as<T7>(from_value));
+ break;
+ case 8:
+ new (to_value) T8(*as<T8>(from_value));
+ break;
+ case 9:
+ new (to_value) T9(*as<T9>(from_value));
+ break;
+ case 10:
+ new (to_value) T10(*as<T10>(from_value));
+ break;
+ case 11:
+ new (to_value) T11(*as<T11>(from_value));
+ break;
+ case 12:
+ new (to_value) T12(*as<T12>(from_value));
+ break;
+ case 13:
+ new (to_value) T13(*as<T13>(from_value));
+ break;
+ case 14:
+ new (to_value) T14(*as<T14>(from_value));
+ break;
+ case 15:
+ new (to_value) T15(*as<T15>(from_value));
+ break;
+ }
+ return from_index;
+ }
+
+ static type_index_t copy_assign(type_index_t const from_index, const void *from_value,
+ void *to_value) {
+ switch (from_index) {
+ case 0:
+ *as<T0>(to_value) = *as<T0>(from_value);
+ break;
+ case 1:
+ *as<T1>(to_value) = *as<T1>(from_value);
+ break;
+ case 2:
+ *as<T2>(to_value) = *as<T2>(from_value);
+ break;
+ case 3:
+ *as<T3>(to_value) = *as<T3>(from_value);
+ break;
+ case 4:
+ *as<T4>(to_value) = *as<T4>(from_value);
+ break;
+ case 5:
+ *as<T5>(to_value) = *as<T5>(from_value);
+ break;
+ case 6:
+ *as<T6>(to_value) = *as<T6>(from_value);
+ break;
+ case 7:
+ *as<T7>(to_value) = *as<T7>(from_value);
+ break;
+ case 8:
+ *as<T8>(to_value) = *as<T8>(from_value);
+ break;
+ case 9:
+ *as<T9>(to_value) = *as<T9>(from_value);
+ break;
+ case 10:
+ *as<T10>(to_value) = *as<T10>(from_value);
+ break;
+ case 11:
+ *as<T11>(to_value) = *as<T11>(from_value);
+ break;
+ case 12:
+ *as<T12>(to_value) = *as<T12>(from_value);
+ break;
+ case 13:
+ *as<T13>(to_value) = *as<T13>(from_value);
+ break;
+ case 14:
+ *as<T14>(to_value) = *as<T14>(from_value);
+ break;
+ case 15:
+ *as<T15>(to_value) = *as<T15>(from_value);
+ break;
+ }
+ return from_index;
+ }
+};
+
+} // namespace detail
+
+//
+// Variant:
+//
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+class variant;
+
+// 19.7.8 Class monostate
+
+class monostate {};
+
+// 19.7.9 monostate relational operators
+
+inline variant_constexpr bool operator<(monostate, monostate) variant_noexcept { return false; }
+inline variant_constexpr bool operator>(monostate, monostate) variant_noexcept { return false; }
+inline variant_constexpr bool operator<=(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator>=(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator==(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator!=(monostate, monostate) variant_noexcept { return false; }
+
+// 19.7.4 variant helper classes
+
+// obtain the size of the variant's list of alternatives at compile time
+
+template <class T> struct variant_size; /* undefined */
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct variant_size<variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+ enum _ {
+ value = detail::typelist_size<variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11,
+ T12, T13, T14, T15)>::value
+ };
+};
+
+#if variant_CPP14_OR_GREATER
+template <class T> constexpr std::size_t variant_size_v = variant_size<T>::value;
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_size_V(T) nonstd::variant_size<T>::value
+#endif
+
+// obtain the type of the alternative specified by its index, at compile time:
+
+template <std::size_t K, class T> struct variant_alternative; /* undefined */
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+struct variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+ typedef typename detail::typelist_type_at<variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9,
+ T10, T11, T12, T13, T14, T15),
+ K>::type type;
+};
+
+#if variant_CPP11_OR_GREATER
+template <std::size_t K, class T>
+using variant_alternative_t = typename variant_alternative<K, T>::type;
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_alternative_T(K, T) typename nonstd::variant_alternative<K, T>::type
+#endif
+
+// NTS:implement specializes the std::uses_allocator type trait
+// std::uses_allocator<nonstd::variant>
+
+// index of the variant in the invalid state (constant)
+
+#if variant_CPP11_OR_GREATER
+variant_constexpr std::size_t variant_npos = static_cast<std::size_t>(-1);
+#else
+static const std::size_t variant_npos = static_cast<std::size_t>(-1);
+#endif
+
+#if !variant_CONFIG_NO_EXCEPTIONS
+
+// 19.7.11 Class bad_variant_access
+
+class bad_variant_access : public std::exception {
+public:
+#if variant_CPP11_OR_GREATER
+ virtual const char *what() const variant_noexcept variant_override
+#else
+ virtual const char *what() const throw()
+#endif
+ {
+ return "bad variant access";
+ }
+};
+
+#endif // variant_CONFIG_NO_EXCEPTIONS
+
+// 19.7.3 Class template variant
+
+template <class T0, class T1 = detail::T1, class T2 = detail::T2, class T3 = detail::T3,
+ class T4 = detail::T4, class T5 = detail::T5, class T6 = detail::T6,
+ class T7 = detail::T7, class T8 = detail::T8, class T9 = detail::T9,
+ class T10 = detail::T10, class T11 = detail::T11, class T12 = detail::T12,
+ class T13 = detail::T13, class T14 = detail::T14, class T15 = detail::T15>
+class variant {
+ typedef detail::helper<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
+ helper_type;
+ typedef variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15) variant_types;
+
+public:
+ // 19.7.3.1 Constructors
+
+ variant() : type_index(0) { new (ptr()) T0(); }
+
+ variant(T0 const &t0) : type_index(0) { new (ptr()) T0(t0); }
+ variant(T1 const &t1) : type_index(1) { new (ptr()) T1(t1); }
+ variant(T2 const &t2) : type_index(2) { new (ptr()) T2(t2); }
+ variant(T3 const &t3) : type_index(3) { new (ptr()) T3(t3); }
+ variant(T4 const &t4) : type_index(4) { new (ptr()) T4(t4); }
+ variant(T5 const &t5) : type_index(5) { new (ptr()) T5(t5); }
+ variant(T6 const &t6) : type_index(6) { new (ptr()) T6(t6); }
+ variant(T7 const &t7) : type_index(7) { new (ptr()) T7(t7); }
+ variant(T8 const &t8) : type_index(8) { new (ptr()) T8(t8); }
+ variant(T9 const &t9) : type_index(9) { new (ptr()) T9(t9); }
+ variant(T10 const &t10) : type_index(10) { new (ptr()) T10(t10); }
+ variant(T11 const &t11) : type_index(11) { new (ptr()) T11(t11); }
+ variant(T12 const &t12) : type_index(12) { new (ptr()) T12(t12); }
+ variant(T13 const &t13) : type_index(13) { new (ptr()) T13(t13); }
+ variant(T14 const &t14) : type_index(14) { new (ptr()) T14(t14); }
+ variant(T15 const &t15) : type_index(15) { new (ptr()) T15(t15); }
+
+#if variant_CPP11_OR_GREATER
+ variant(T0 &&t0) : type_index(0) { new (ptr()) T0(std::move(t0)); }
+ variant(T1 &&t1) : type_index(1) { new (ptr()) T1(std::move(t1)); }
+ variant(T2 &&t2) : type_index(2) { new (ptr()) T2(std::move(t2)); }
+ variant(T3 &&t3) : type_index(3) { new (ptr()) T3(std::move(t3)); }
+ variant(T4 &&t4) : type_index(4) { new (ptr()) T4(std::move(t4)); }
+ variant(T5 &&t5) : type_index(5) { new (ptr()) T5(std::move(t5)); }
+ variant(T6 &&t6) : type_index(6) { new (ptr()) T6(std::move(t6)); }
+ variant(T7 &&t7) : type_index(7) { new (ptr()) T7(std::move(t7)); }
+ variant(T8 &&t8) : type_index(8) { new (ptr()) T8(std::move(t8)); }
+ variant(T9 &&t9) : type_index(9) { new (ptr()) T9(std::move(t9)); }
+ variant(T10 &&t10) : type_index(10) { new (ptr()) T10(std::move(t10)); }
+ variant(T11 &&t11) : type_index(11) { new (ptr()) T11(std::move(t11)); }
+ variant(T12 &&t12) : type_index(12) { new (ptr()) T12(std::move(t12)); }
+ variant(T13 &&t13) : type_index(13) { new (ptr()) T13(std::move(t13)); }
+ variant(T14 &&t14) : type_index(14) { new (ptr()) T14(std::move(t14)); }
+ variant(T15 &&t15) : type_index(15) { new (ptr()) T15(std::move(t15)); }
+
+#endif
+
+ variant(variant const &other) : type_index(other.type_index) {
+ (void)helper_type::copy_construct(other.type_index, other.ptr(), ptr());
+ }
+
+#if variant_CPP11_OR_GREATER
+
+ variant(variant &&other) noexcept(
+ std::is_nothrow_move_constructible<T0>::value &&std::is_nothrow_move_constructible<T1>::value
+ &&std::is_nothrow_move_constructible<T2>::value &&std::is_nothrow_move_constructible<
+ T3>::value &&std::is_nothrow_move_constructible<T4>::value
+ &&std::is_nothrow_move_constructible<T5>::value &&std::is_nothrow_move_constructible<
+ T6>::value &&std::is_nothrow_move_constructible<T7>::value
+ &&std::is_nothrow_move_constructible<T8>::value
+ &&std::is_nothrow_move_constructible<T9>::value
+ &&std::is_nothrow_move_constructible<T10>::value
+ &&std::is_nothrow_move_constructible<T11>::value
+ &&std::is_nothrow_move_constructible<T12>::value
+ &&std::is_nothrow_move_constructible<T13>::value
+ &&std::is_nothrow_move_constructible<T14>::value
+ &&std::is_nothrow_move_constructible<T15>::value)
+ : type_index(other.type_index) {
+ (void)helper_type::move_construct(other.type_index, other.ptr(), ptr());
+ }
+
+ template <std::size_t K>
+ using type_at_t = typename detail::typelist_type_at<variant_types, K>::type;
+
+ template <class T, class... Args variant_REQUIRES_T(std::is_constructible<T, Args...>::value)>
+ explicit variant(nonstd_lite_in_place_type_t(T), Args &&... args) {
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_t<T>(ptr(), std::forward<Args>(args)...);
+ }
+
+ template <class T, class U,
+ class... Args variant_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U> &, Args...>::value)>
+ explicit variant(nonstd_lite_in_place_type_t(T), std::initializer_list<U> il, Args &&... args) {
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_t<T>(ptr(), il, std::forward<Args>(args)...);
+ }
+
+ template <std::size_t K,
+ class... Args variant_REQUIRES_T(std::is_constructible<type_at_t<K>, Args...>::value)>
+ explicit variant(nonstd_lite_in_place_index_t(K), Args &&... args) {
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_i<K>(ptr(), std::forward<Args>(args)...);
+ }
+
+ template <size_t K, class U,
+ class... Args variant_REQUIRES_T(
+ std::is_constructible<type_at_t<K>, std::initializer_list<U> &, Args...>::value)>
+ explicit variant(nonstd_lite_in_place_index_t(K), std::initializer_list<U> il, Args &&... args) {
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_i<K>(ptr(), il, std::forward<Args>(args)...);
+ }
+
+#endif // variant_CPP11_OR_GREATER
+
+ // 19.7.3.2 Destructor
+
+ ~variant() {
+ if (!valueless_by_exception()) {
+ helper_type::destroy(type_index, ptr());
+ }
+ }
+
+ // 19.7.3.3 Assignment
+
+ variant &operator=(variant const &other) { return copy_assign(other); }
+
+#if variant_CPP11_OR_GREATER
+
+ variant &operator=(variant &&other) noexcept(
+ std::is_nothrow_move_assignable<T0>::value &&std::is_nothrow_move_assignable<T1>::value
+ &&std::is_nothrow_move_assignable<T2>::value &&std::is_nothrow_move_assignable<T3>::value
+ &&std::is_nothrow_move_assignable<T4>::value &&std::is_nothrow_move_assignable<
+ T5>::value &&std::is_nothrow_move_assignable<T6>::value
+ &&std::is_nothrow_move_assignable<T7>::value &&std::is_nothrow_move_assignable<
+ T8>::value &&std::is_nothrow_move_assignable<T9>::value &&
+ std::is_nothrow_move_assignable<T10>::value &&std::is_nothrow_move_assignable<
+ T11>::value &&std::is_nothrow_move_assignable<T12>::value
+ &&std::is_nothrow_move_assignable<T13>::value
+ &&std::is_nothrow_move_assignable<T14>::value
+ &&std::is_nothrow_move_assignable<T15>::value) {
+ return move_assign(std::move(other));
+ }
+
+ variant &operator=(T0 &&t0) { return assign_value<0>(std::move(t0)); }
+ variant &operator=(T1 &&t1) { return assign_value<1>(std::move(t1)); }
+ variant &operator=(T2 &&t2) { return assign_value<2>(std::move(t2)); }
+ variant &operator=(T3 &&t3) { return assign_value<3>(std::move(t3)); }
+ variant &operator=(T4 &&t4) { return assign_value<4>(std::move(t4)); }
+ variant &operator=(T5 &&t5) { return assign_value<5>(std::move(t5)); }
+ variant &operator=(T6 &&t6) { return assign_value<6>(std::move(t6)); }
+ variant &operator=(T7 &&t7) { return assign_value<7>(std::move(t7)); }
+ variant &operator=(T8 &&t8) { return assign_value<8>(std::move(t8)); }
+ variant &operator=(T9 &&t9) { return assign_value<9>(std::move(t9)); }
+ variant &operator=(T10 &&t10) { return assign_value<10>(std::move(t10)); }
+ variant &operator=(T11 &&t11) { return assign_value<11>(std::move(t11)); }
+ variant &operator=(T12 &&t12) { return assign_value<12>(std::move(t12)); }
+ variant &operator=(T13 &&t13) { return assign_value<13>(std::move(t13)); }
+ variant &operator=(T14 &&t14) { return assign_value<14>(std::move(t14)); }
+ variant &operator=(T15 &&t15) { return assign_value<15>(std::move(t15)); }
+
+#endif
+
+ variant &operator=(T0 const &t0) { return assign_value<0>(t0); }
+ variant &operator=(T1 const &t1) { return assign_value<1>(t1); }
+ variant &operator=(T2 const &t2) { return assign_value<2>(t2); }
+ variant &operator=(T3 const &t3) { return assign_value<3>(t3); }
+ variant &operator=(T4 const &t4) { return assign_value<4>(t4); }
+ variant &operator=(T5 const &t5) { return assign_value<5>(t5); }
+ variant &operator=(T6 const &t6) { return assign_value<6>(t6); }
+ variant &operator=(T7 const &t7) { return assign_value<7>(t7); }
+ variant &operator=(T8 const &t8) { return assign_value<8>(t8); }
+ variant &operator=(T9 const &t9) { return assign_value<9>(t9); }
+ variant &operator=(T10 const &t10) { return assign_value<10>(t10); }
+ variant &operator=(T11 const &t11) { return assign_value<11>(t11); }
+ variant &operator=(T12 const &t12) { return assign_value<12>(t12); }
+ variant &operator=(T13 const &t13) { return assign_value<13>(t13); }
+ variant &operator=(T14 const &t14) { return assign_value<14>(t14); }
+ variant &operator=(T15 const &t15) { return assign_value<15>(t15); }
+
+ std::size_t index() const {
+ return variant_npos_internal() == type_index ? variant_npos
+ : static_cast<std::size_t>(type_index);
+ }
+
+ // 19.7.3.4 Modifiers
+
+#if variant_CPP11_OR_GREATER
+ template <class T, class... Args variant_REQUIRES_T(std::is_constructible<T, Args...>::value)>
+ T &emplace(Args &&... args) {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_t<T>(ptr(), std::forward<Args>(args)...);
+
+ return *as<T>();
+ }
+
+ template <class T, class U,
+ class... Args variant_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U> &, Args...>::value)>
+ T &emplace(std::initializer_list<U> il, Args &&... args) {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_t<T>(ptr(), il, std::forward<Args>(args)...);
+
+ return *as<T>();
+ }
+
+ template <size_t K,
+ class... Args variant_REQUIRES_T(std::is_constructible<type_at_t<K>, Args...>::value)>
+ variant_alternative_t<K, variant> &emplace(Args &&... args) {
+ return this->template emplace<type_at_t<K>>(std::forward<Args>(args)...);
+ }
+
+ template <size_t K, class U,
+ class... Args variant_REQUIRES_T(
+ std::is_constructible<type_at_t<K>, std::initializer_list<U> &, Args...>::value)>
+ variant_alternative_t<K, variant> &emplace(std::initializer_list<U> il, Args &&... args) {
+ return this->template emplace<type_at_t<K>>(il, std::forward<Args>(args)...);
+ }
+
+#endif // variant_CPP11_OR_GREATER
+
+ // 19.7.3.5 Value status
+
+ bool valueless_by_exception() const { return type_index == variant_npos_internal(); }
+
+ // 19.7.3.6 Swap
+
+ void swap(variant &other)
+#if variant_CPP11_OR_GREATER
+ noexcept(
+ std::is_nothrow_move_constructible<T0>::value &&std17::is_nothrow_swappable<
+ T0>::value &&std::is_nothrow_move_constructible<T1>::value
+ &&std17::is_nothrow_swappable<T1>::value &&std::is_nothrow_move_constructible<
+ T2>::value &&std17::is_nothrow_swappable<T2>::value
+ &&std::is_nothrow_move_constructible<T3>::value &&std17::is_nothrow_swappable<
+ T3>::value &&std::is_nothrow_move_constructible<T4>::value
+ &&std17::is_nothrow_swappable<T4>::value &&std::is_nothrow_move_constructible<
+ T5>::value &&std17::is_nothrow_swappable<T5>::value &&std::
+ is_nothrow_move_constructible<T6>::value &&std17::is_nothrow_swappable<
+ T6>::value &&std::is_nothrow_move_constructible<T7>::value &&std17::
+ is_nothrow_swappable<T7>::value &&std::is_nothrow_move_constructible<
+ T8>::value &&std17::is_nothrow_swappable<T8>::value
+ &&std::is_nothrow_move_constructible<
+ T9>::value &&std17::is_nothrow_swappable<T9>::value
+ &&std::is_nothrow_move_constructible<
+ T10>::value &&std17::is_nothrow_swappable<T10>::value
+ &&std::is_nothrow_move_constructible<
+ T11>::value &&std17::is_nothrow_swappable<T11>::value
+ &&std::is_nothrow_move_constructible<T12>::value
+ &&std17::is_nothrow_swappable<T12>::value &&
+ std::is_nothrow_move_constructible<T13>::value
+ &&std17::is_nothrow_swappable<T13>::value
+ &&std::is_nothrow_move_constructible<
+ T14>::value
+ &&std17::is_nothrow_swappable<
+ T14>::value &&std::
+ is_nothrow_move_constructible<
+ T15>::value &&std17::
+ is_nothrow_swappable<
+ T15>::value
+
+ )
+#endif
+ {
+ if (valueless_by_exception() && other.valueless_by_exception()) {
+ // no effect
+ } else if (type_index == other.type_index) {
+ this->swap_value(type_index, other);
+ } else {
+#if variant_CPP11_OR_GREATER
+ variant tmp(std::move(*this));
+ *this = std::move(other);
+ other = std::move(tmp);
+#else
+ variant tmp(*this);
+ *this = other;
+ other = tmp;
+#endif
+ }
+ }
+
+ //
+ // non-standard:
+ //
+
+ template <class T> static variant_constexpr std::size_t index_of() variant_noexcept {
+ return to_size_t(
+ detail::typelist_index_of<variant_types, typename std11::remove_cv<T>::type>::value);
+ }
+
+ template <class T> T &get() {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(index_of<T>() == index());
+#else
+ if (index_of<T>() != index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return *as<T>();
+ }
+
+ template <class T> T const &get() const {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(index_of<T>() == index());
+#else
+ if (index_of<T>() != index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return *as<const T>();
+ }
+
+ template <std::size_t K> typename variant_alternative<K, variant>::type &get() {
+ return this->template get<typename detail::typelist_type_at<variant_types, K>::type>();
+ }
+
+ template <std::size_t K> typename variant_alternative<K, variant>::type const &get() const {
+ return this->template get<typename detail::typelist_type_at<variant_types, K>::type>();
+ }
+
+private:
+ typedef typename helper_type::type_index_t type_index_t;
+
+ void *ptr() variant_noexcept { return &data; }
+
+ void const *ptr() const variant_noexcept { return &data; }
+
+ template <class U> U *as() { return reinterpret_cast<U *>(ptr()); }
+
+ template <class U> U const *as() const { return reinterpret_cast<U const *>(ptr()); }
+
+ template <class U> static variant_constexpr std::size_t to_size_t(U index) {
+ return static_cast<std::size_t>(index);
+ }
+
+ variant_constexpr type_index_t variant_npos_internal() const variant_noexcept {
+ return static_cast<type_index_t>(-1);
+ }
+
+ variant ©_assign(variant const &other) {
+ if (valueless_by_exception() && other.valueless_by_exception()) {
+ // no effect
+ } else if (!valueless_by_exception() && other.valueless_by_exception()) {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ } else if (index() == other.index()) {
+ type_index = helper_type::copy_assign(other.type_index, other.ptr(), ptr());
+ } else {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ type_index = helper_type::copy_construct(other.type_index, other.ptr(), ptr());
+ }
+ return *this;
+ }
+
+#if variant_CPP11_OR_GREATER
+
+ variant &move_assign(variant &&other) {
+ if (valueless_by_exception() && other.valueless_by_exception()) {
+ // no effect
+ } else if (!valueless_by_exception() && other.valueless_by_exception()) {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ } else if (index() == other.index()) {
+ type_index = helper_type::move_assign(other.type_index, other.ptr(), ptr());
+ } else {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ type_index = helper_type::move_construct(other.type_index, other.ptr(), ptr());
+ }
+ return *this;
+ }
+
+ template <std::size_t K, class T> variant &assign_value(T &&value) {
+ if (index() == K) {
+ *as<T>() = std::forward<T>(value);
+ } else {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ new (ptr()) T(std::forward<T>(value));
+ type_index = K;
+ }
+ return *this;
+ }
+
+#endif // variant_CPP11_OR_GREATER
+
+ template <std::size_t K, class T> variant &assign_value(T const &value) {
+ if (index() == K) {
+ *as<T>() = value;
+ } else {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ new (ptr()) T(value);
+ type_index = K;
+ }
+ return *this;
+ }
+
+ void swap_value(type_index_t index, variant &other) {
+ using std::swap;
+ switch (index) {
+ case 0:
+ swap(this->get<0>(), other.get<0>());
+ break;
+ case 1:
+ swap(this->get<1>(), other.get<1>());
+ break;
+ case 2:
+ swap(this->get<2>(), other.get<2>());
+ break;
+ case 3:
+ swap(this->get<3>(), other.get<3>());
+ break;
+ case 4:
+ swap(this->get<4>(), other.get<4>());
+ break;
+ case 5:
+ swap(this->get<5>(), other.get<5>());
+ break;
+ case 6:
+ swap(this->get<6>(), other.get<6>());
+ break;
+ case 7:
+ swap(this->get<7>(), other.get<7>());
+ break;
+ case 8:
+ swap(this->get<8>(), other.get<8>());
+ break;
+ case 9:
+ swap(this->get<9>(), other.get<9>());
+ break;
+ case 10:
+ swap(this->get<10>(), other.get<10>());
+ break;
+ case 11:
+ swap(this->get<11>(), other.get<11>());
+ break;
+ case 12:
+ swap(this->get<12>(), other.get<12>());
+ break;
+ case 13:
+ swap(this->get<13>(), other.get<13>());
+ break;
+ case 14:
+ swap(this->get<14>(), other.get<14>());
+ break;
+ case 15:
+ swap(this->get<15>(), other.get<15>());
+ break;
+ }
+ }
+
+private:
+ enum { data_size = detail::typelist_max<variant_types>::value };
+
+#if variant_CPP11_OR_GREATER
+
+ enum { data_align = detail::typelist_max_alignof<variant_types>::value };
+
+ using aligned_storage_t = typename std::aligned_storage<data_size, data_align>::type;
+ aligned_storage_t data;
+
+#elif variant_CONFIG_MAX_ALIGN_HACK
+
+ typedef union {
+ unsigned char data[data_size];
+ } aligned_storage_t;
+
+ detail::max_align_t hack;
+ aligned_storage_t data;
+
+#else
+ typedef typename detail::typelist_max<variant_types>::type max_type;
+
+ typedef variant_ALIGN_AS(max_type) align_as_type;
+
+ typedef union {
+ align_as_type data[1 + (data_size - 1) / sizeof(align_as_type)];
+ } aligned_storage_t;
+ aligned_storage_t data;
+
+ // # undef variant_ALIGN_AS
+
+#endif // variant_CONFIG_MAX_ALIGN_HACK
+
+ type_index_t type_index;
+};
+
+// 19.7.5 Value access
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool holds_alternative(
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v)
+ variant_noexcept {
+ return v.index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15>::template index_of<T>();
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R &get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &v,
+ nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+ return v.template get<R>();
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R const &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+ return v.template get<R>();
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &v,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(K == v.index());
+#else
+ if (K != v.index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return v.template get<K>();
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type const &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(K == v.index());
+#else
+ if (K != v.index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return v.template get<K>();
+}
+
+#if variant_CPP11_OR_GREATER
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R &&get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &&v,
+ nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+ return std::move(v.template get<R>());
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R const &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &&v,
+ nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+ return std::move(v.template get<R>());
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &&v,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(K == v.index());
+#else
+ if (K != v.index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return std::move(v.template get<K>());
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type const &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &&v,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(K == v.index());
+#else
+ if (K != v.index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return std::move(v.template get<K>());
+}
+
+#endif // variant_CPP11_OR_GREATER
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline typename std11::add_pointer<T>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> *pv,
+ nonstd_lite_in_place_type_t(T) = nonstd_lite_in_place_type(T)) {
+ return (pv->index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15>::template index_of<T>())
+ ? &get<T>(*pv)
+ : variant_nullptr;
+}
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline typename std11::add_pointer<const T>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const *pv,
+ nonstd_lite_in_place_type_t(T) = nonstd_lite_in_place_type(T)) {
+ return (pv->index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15>::template index_of<T>())
+ ? &get<T>(*pv)
+ : variant_nullptr;
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename std11::add_pointer<typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> *pv,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+ return (pv->index() == K) ? &get<K>(*pv) : variant_nullptr;
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename std11::add_pointer<const typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const *pv,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+ return (pv->index() == K) ? &get<K>(*pv) : variant_nullptr;
+}
+
+// 19.7.10 Specialized algorithms
+
+template <
+ class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15
+#if variant_CPP11_OR_GREATER
+ variant_REQUIRES_T(
+ std::is_move_constructible<T0>::value &&std17::is_swappable<
+ T0>::value &&std::is_move_constructible<T1>::value &&std17::is_swappable<T1>::value
+ &&std::is_move_constructible<T2>::value &&std17::is_swappable<
+ T2>::value &&std::is_move_constructible<T3>::value &&std17::is_swappable<T3>::
+ value &&std::is_move_constructible<T4>::value &&std17::is_swappable<T4>::value
+ &&std::is_move_constructible<T5>::value &&std17::is_swappable<
+ T5>::value &&std::is_move_constructible<T6>::value
+ &&std17::is_swappable<T6>::value &&std::is_move_constructible<
+ T7>::value &&std17::is_swappable<T7>::value
+ &&std::is_move_constructible<T8>::value &&std17::is_swappable<
+ T8>::value &&std::is_move_constructible<T9>::value
+ &&std17::is_swappable<T9>::value &&std::is_move_constructible<
+ T10>::value &&std17::is_swappable<T10>::value &&std::
+ is_move_constructible<T11>::value &&std17::is_swappable<
+ T11>::value &&std::is_move_constructible<T12>::value
+ &&std17::is_swappable<
+ T12>::value &&std::is_move_constructible<T13>::value
+ &&std17::is_swappable<T13>::value
+ &&std::is_move_constructible<T14>::value
+ &&std17::is_swappable<T14>::value
+ &&std::is_move_constructible<T15>::value
+ &&std17::is_swappable<T15>::value)
+#endif
+ >
+inline void swap(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &a,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &b)
+#if variant_CPP11_OR_GREATER
+ noexcept(noexcept(a.swap(b)))
+#endif
+{
+ a.swap(b);
+}
+
+// 19.7.7 Visitation
+
+// Variant 'visitor' implementation
+
+namespace detail {
+
+template <typename R, typename VT> struct VisitorApplicatorImpl {
+ template <typename Visitor, typename T> static R apply(Visitor const &v, T const &arg) {
+ return v(arg);
+ }
+};
+
+template <typename R, typename VT> struct VisitorApplicatorImpl<R, TX<VT>> {
+ template <typename Visitor, typename T> static R apply(Visitor const &, T) {
+ // prevent default construction of a const reference, see issue #39:
+ std::terminate();
+ }
+};
+
+template <typename R> struct VisitorApplicator;
+
+template <typename R, typename Visitor, typename V1> struct VisitorUnwrapper;
+
+#if variant_CPP11_OR_GREATER
+template <size_t NumVars, typename R, typename Visitor, typename... T>
+#else
+template <size_t NumVars, typename R, typename Visitor, typename T1, typename T2 = S0,
+ typename T3 = S0, typename T4 = S0, typename T5 = S0>
+#endif
+struct TypedVisitorUnwrapper;
+
+template <typename R, typename Visitor, typename T2>
+struct TypedVisitorUnwrapper<2, R, Visitor, T2> {
+ const Visitor &visitor;
+ T2 const &val2;
+
+ TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_)
+ : visitor(visitor_), val2(val2_)
+
+ {}
+
+ template <typename T> R operator()(const T &val1) const { return visitor(val1, val2); }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3>
+struct TypedVisitorUnwrapper<3, R, Visitor, T2, T3> {
+ const Visitor &visitor;
+ T2 const &val2;
+ T3 const &val3;
+
+ TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_)
+ : visitor(visitor_), val2(val2_), val3(val3_)
+
+ {}
+
+ template <typename T> R operator()(const T &val1) const { return visitor(val1, val2, val3); }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3, typename T4>
+struct TypedVisitorUnwrapper<4, R, Visitor, T2, T3, T4> {
+ const Visitor &visitor;
+ T2 const &val2;
+ T3 const &val3;
+ T4 const &val4;
+
+ TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_, T4 const &val4_)
+ : visitor(visitor_), val2(val2_), val3(val3_), val4(val4_)
+
+ {}
+
+ template <typename T> R operator()(const T &val1) const {
+ return visitor(val1, val2, val3, val4);
+ }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3, typename T4, typename T5>
+struct TypedVisitorUnwrapper<5, R, Visitor, T2, T3, T4, T5> {
+ const Visitor &visitor;
+ T2 const &val2;
+ T3 const &val3;
+ T4 const &val4;
+ T5 const &val5;
+
+ TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_, T4 const &val4_,
+ T5 const &val5_)
+ : visitor(visitor_), val2(val2_), val3(val3_), val4(val4_), val5(val5_)
+
+ {}
+
+ template <typename T> R operator()(const T &val1) const {
+ return visitor(val1, val2, val3, val4, val5);
+ }
+};
+
+template <typename R, typename Visitor, typename V2> struct VisitorUnwrapper {
+ const Visitor &visitor;
+ const V2 &r;
+
+ VisitorUnwrapper(const Visitor &visitor_, const V2 &r_) : visitor(visitor_), r(r_) {}
+
+ template <typename T1> R operator()(T1 const &val1) const {
+ typedef TypedVisitorUnwrapper<2, R, Visitor, T1> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1), r);
+ }
+
+ template <typename T1, typename T2> R operator()(T1 const &val1, T2 const &val2) const {
+ typedef TypedVisitorUnwrapper<3, R, Visitor, T1, T2> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2), r);
+ }
+
+ template <typename T1, typename T2, typename T3>
+ R operator()(T1 const &val1, T2 const &val2, T3 const &val3) const {
+ typedef TypedVisitorUnwrapper<4, R, Visitor, T1, T2, T3> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3), r);
+ }
+
+ template <typename T1, typename T2, typename T3, typename T4>
+ R operator()(T1 const &val1, T2 const &val2, T3 const &val3, T4 const &val4) const {
+ typedef TypedVisitorUnwrapper<5, R, Visitor, T1, T2, T3, T4> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3, val4), r);
+ }
+
+ template <typename T1, typename T2, typename T3, typename T4, typename T5>
+ R operator()(T1 const &val1, T2 const &val2, T3 const &val3, T4 const &val4,
+ T5 const &val5) const {
+ typedef TypedVisitorUnwrapper<6, R, Visitor, T1, T2, T3, T4, T5> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3, val4, val5), r);
+ }
+};
+
+template <typename R> struct VisitorApplicator {
+ template <typename Visitor, typename V1> static R apply(const Visitor &v, const V1 &arg) {
+ switch (arg.index()) {
+ case 0:
+ return apply_visitor<0>(v, arg);
+ case 1:
+ return apply_visitor<1>(v, arg);
+ case 2:
+ return apply_visitor<2>(v, arg);
+ case 3:
+ return apply_visitor<3>(v, arg);
+ case 4:
+ return apply_visitor<4>(v, arg);
+ case 5:
+ return apply_visitor<5>(v, arg);
+ case 6:
+ return apply_visitor<6>(v, arg);
+ case 7:
+ return apply_visitor<7>(v, arg);
+ case 8:
+ return apply_visitor<8>(v, arg);
+ case 9:
+ return apply_visitor<9>(v, arg);
+ case 10:
+ return apply_visitor<10>(v, arg);
+ case 11:
+ return apply_visitor<11>(v, arg);
+ case 12:
+ return apply_visitor<12>(v, arg);
+ case 13:
+ return apply_visitor<13>(v, arg);
+ case 14:
+ return apply_visitor<14>(v, arg);
+ case 15:
+ return apply_visitor<15>(v, arg);
+
+ // prevent default construction of a const reference, see issue #39:
+ default:
+ std::terminate();
+ }
+ }
+
+ template <size_t Idx, typename Visitor, typename V1>
+ static R apply_visitor(const Visitor &v, const V1 &arg) {
+
+#if variant_CPP11_OR_GREATER
+ typedef typename variant_alternative<Idx, typename std::decay<V1>::type>::type value_type;
+#else
+ typedef typename variant_alternative<Idx, V1>::type value_type;
+#endif
+ return VisitorApplicatorImpl<R, value_type>::apply(v, get<Idx>(arg));
+ }
+
+#if variant_CPP11_OR_GREATER
+ template <typename Visitor, typename V1, typename V2, typename... V>
+ static R apply(const Visitor &v, const V1 &arg1, const V2 &arg2, const V... args) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2, args...);
+ }
+#else
+
+ template <typename Visitor, typename V1, typename V2>
+ static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2);
+ }
+
+ template <typename Visitor, typename V1, typename V2, typename V3>
+ static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2, arg3);
+ }
+
+ template <typename Visitor, typename V1, typename V2, typename V3, typename V4>
+ static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2, arg3, arg4);
+ }
+
+ template <typename Visitor, typename V1, typename V2, typename V3, typename V4, typename V5>
+ static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4,
+ V5 const &arg5) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2, arg3, arg4, arg5);
+ }
+
+#endif
+};
+
+#if variant_CPP11_OR_GREATER
+template <size_t NumVars, typename Visitor, typename... V> struct VisitorImpl {
+ typedef decltype(
+ std::declval<Visitor>()(get<0>(static_cast<const V &>(std::declval<V>()))...)) result_type;
+ typedef VisitorApplicator<result_type> applicator_type;
+};
+#endif
+} // namespace detail
+
+#if variant_CPP11_OR_GREATER
+// No perfect forwarding here in order to simplify code
+template <typename Visitor, typename... V>
+inline auto visit(Visitor const &v, V const &... vars) ->
+ typename detail::VisitorImpl<sizeof...(V), Visitor, V...>::result_type {
+ typedef detail::VisitorImpl<sizeof...(V), Visitor, V...> impl_type;
+ return impl_type::applicator_type::apply(v, vars...);
+}
+#else
+
+template <typename R, typename Visitor, typename V1>
+inline R visit(const Visitor &v, V1 const &arg1) {
+ return detail::VisitorApplicator<R>::apply(v, arg1);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2) {
+ return detail::VisitorApplicator<R>::apply(v, arg1, arg2);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3) {
+ return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3, typename V4>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4) {
+ return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3, arg4);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3, typename V4,
+ typename V5>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4,
+ V5 const &arg5) {
+ return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3, arg4, arg5);
+}
+
+#endif
+
+// 19.7.6 Relational operators
+
+namespace detail {
+
+template <class Variant> struct Comparator {
+ static inline bool equal(Variant const &v, Variant const &w) {
+ switch (v.index()) {
+ case 0:
+ return get<0>(v) == get<0>(w);
+ case 1:
+ return get<1>(v) == get<1>(w);
+ case 2:
+ return get<2>(v) == get<2>(w);
+ case 3:
+ return get<3>(v) == get<3>(w);
+ case 4:
+ return get<4>(v) == get<4>(w);
+ case 5:
+ return get<5>(v) == get<5>(w);
+ case 6:
+ return get<6>(v) == get<6>(w);
+ case 7:
+ return get<7>(v) == get<7>(w);
+ case 8:
+ return get<8>(v) == get<8>(w);
+ case 9:
+ return get<9>(v) == get<9>(w);
+ case 10:
+ return get<10>(v) == get<10>(w);
+ case 11:
+ return get<11>(v) == get<11>(w);
+ case 12:
+ return get<12>(v) == get<12>(w);
+ case 13:
+ return get<13>(v) == get<13>(w);
+ case 14:
+ return get<14>(v) == get<14>(w);
+ case 15:
+ return get<15>(v) == get<15>(w);
+
+ default:
+ return false;
+ }
+ }
+
+ static inline bool less_than(Variant const &v, Variant const &w) {
+ switch (v.index()) {
+ case 0:
+ return get<0>(v) < get<0>(w);
+ case 1:
+ return get<1>(v) < get<1>(w);
+ case 2:
+ return get<2>(v) < get<2>(w);
+ case 3:
+ return get<3>(v) < get<3>(w);
+ case 4:
+ return get<4>(v) < get<4>(w);
+ case 5:
+ return get<5>(v) < get<5>(w);
+ case 6:
+ return get<6>(v) < get<6>(w);
+ case 7:
+ return get<7>(v) < get<7>(w);
+ case 8:
+ return get<8>(v) < get<8>(w);
+ case 9:
+ return get<9>(v) < get<9>(w);
+ case 10:
+ return get<10>(v) < get<10>(w);
+ case 11:
+ return get<11>(v) < get<11>(w);
+ case 12:
+ return get<12>(v) < get<12>(w);
+ case 13:
+ return get<13>(v) < get<13>(w);
+ case 14:
+ return get<14>(v) < get<14>(w);
+ case 15:
+ return get<15>(v) < get<15>(w);
+
+ default:
+ return false;
+ }
+ }
+};
+
+} // namespace detail
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator==(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ if (v.index() != w.index())
+ return false;
+ else if (v.valueless_by_exception())
+ return true;
+ else
+ return detail::Comparator<
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::equal(v, w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator!=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ return !(v == w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator<(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ if (w.valueless_by_exception())
+ return false;
+ else if (v.valueless_by_exception())
+ return true;
+ else if (v.index() < w.index())
+ return true;
+ else if (v.index() > w.index())
+ return false;
+ else
+ return detail::Comparator<variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
+ T14, T15>>::less_than(v, w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator>(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ return w < v;
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator<=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ return !(v > w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator>=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ return !(v < w);
+}
+
+} // namespace variants
+
+using namespace variants;
+
+} // namespace nonstd
+
+#if variant_CPP11_OR_GREATER
+
+// 19.7.12 Hash support
+
+namespace std {
+
+template <> struct hash<nonstd::monostate> {
+ std::size_t operator()(nonstd::monostate) const variant_noexcept { return 42; }
+};
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct hash<nonstd::variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+ std::size_t operator()(nonstd::variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
+ T14, T15> const &v) const variant_noexcept {
+ namespace nvd = nonstd::variants::detail;
+
+ switch (v.index()) {
+ case 0:
+ return nvd::hash(0) ^ nvd::hash(get<0>(v));
+ case 1:
+ return nvd::hash(1) ^ nvd::hash(get<1>(v));
+ case 2:
+ return nvd::hash(2) ^ nvd::hash(get<2>(v));
+ case 3:
+ return nvd::hash(3) ^ nvd::hash(get<3>(v));
+ case 4:
+ return nvd::hash(4) ^ nvd::hash(get<4>(v));
+ case 5:
+ return nvd::hash(5) ^ nvd::hash(get<5>(v));
+ case 6:
+ return nvd::hash(6) ^ nvd::hash(get<6>(v));
+ case 7:
+ return nvd::hash(7) ^ nvd::hash(get<7>(v));
+ case 8:
+ return nvd::hash(8) ^ nvd::hash(get<8>(v));
+ case 9:
+ return nvd::hash(9) ^ nvd::hash(get<9>(v));
+ case 10:
+ return nvd::hash(10) ^ nvd::hash(get<10>(v));
+ case 11:
+ return nvd::hash(11) ^ nvd::hash(get<11>(v));
+ case 12:
+ return nvd::hash(12) ^ nvd::hash(get<12>(v));
+ case 13:
+ return nvd::hash(13) ^ nvd::hash(get<13>(v));
+ case 14:
+ return nvd::hash(14) ^ nvd::hash(get<14>(v));
+ case 15:
+ return nvd::hash(15) ^ nvd::hash(get<15>(v));
+
+ default:
+ return 0;
+ }
+ }
+};
+
+} // namespace std
+
+#endif // variant_CPP11_OR_GREATER
+
+#if variant_BETWEEN(variant_COMPILER_MSVC_VER, 1300, 1900)
+#pragma warning(pop)
+#endif
+
+#endif // variant_USES_STD_VARIANT
+
+#endif // NONSTD_VARIANT_LITE_HPP
+//
+// Copyright (c) 2014-2018 Martin Moene
+//
+// https://github.com/martinmoene/optional-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_OPTIONAL_LITE_HPP
+#define NONSTD_OPTIONAL_LITE_HPP
+
+#define optional_lite_MAJOR 3
+#define optional_lite_MINOR 2
+#define optional_lite_PATCH 0
+
+#define optional_lite_VERSION \
+ optional_STRINGIFY(optional_lite_MAJOR) "." optional_STRINGIFY( \
+ optional_lite_MINOR) "." optional_STRINGIFY(optional_lite_PATCH)
+
+#define optional_STRINGIFY(x) optional_STRINGIFY_(x)
+#define optional_STRINGIFY_(x) #x
+
+// optional-lite configuration:
+
+#define optional_OPTIONAL_DEFAULT 0
+#define optional_OPTIONAL_NONSTD 1
+#define optional_OPTIONAL_STD 2
+
+#if !defined(optional_CONFIG_SELECT_OPTIONAL)
+#define optional_CONFIG_SELECT_OPTIONAL \
+ (optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD)
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef optional_CONFIG_NO_EXCEPTIONS
+#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define optional_CONFIG_NO_EXCEPTIONS 0
+#else
+#define optional_CONFIG_NO_EXCEPTIONS 1
+#endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef optional_CPLUSPLUS
+#if defined(_MSVC_LANG) && !defined(__clang__)
+#define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
+#else
+#define optional_CPLUSPLUS __cplusplus
+#endif
+#endif
+
+#define optional_CPP98_OR_GREATER (optional_CPLUSPLUS >= 199711L)
+#define optional_CPP11_OR_GREATER (optional_CPLUSPLUS >= 201103L)
+#define optional_CPP11_OR_GREATER_ (optional_CPLUSPLUS >= 201103L)
+#define optional_CPP14_OR_GREATER (optional_CPLUSPLUS >= 201402L)
+#define optional_CPP17_OR_GREATER (optional_CPLUSPLUS >= 201703L)
+#define optional_CPP20_OR_GREATER (optional_CPLUSPLUS >= 202000L)
+
+// C++ language version (represent 98 as 3):
+
+#define optional_CPLUSPLUS_V \
+ (optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994))
+
+// Use C++17 std::optional if available and requested:
+
+#if optional_CPP17_OR_GREATER && defined(__has_include)
+#if __has_include(<optional> )
+#define optional_HAVE_STD_OPTIONAL 1
+#else
+#define optional_HAVE_STD_OPTIONAL 0
+#endif
+#else
+#define optional_HAVE_STD_OPTIONAL 0
+#endif
+
+#define optional_USES_STD_OPTIONAL \
+ ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) || \
+ ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL))
+
+//
+// in_place: code duplicated in any-lite, expected-lite, optional-lite, value-ptr-lite,
+// variant-lite:
+//
+
+#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES
+#define nonstd_lite_HAVE_IN_PLACE_TYPES 1
+
+// C++17 std::in_place in <utility>:
+
+#if optional_CPP17_OR_GREATER
+
+#include <utility>
+
+namespace nonstd {
+
+using std::in_place;
+using std::in_place_index;
+using std::in_place_index_t;
+using std::in_place_t;
+using std::in_place_type;
+using std::in_place_type_t;
+
+#define nonstd_lite_in_place_t(T) std::in_place_t
+#define nonstd_lite_in_place_type_t(T) std::in_place_type_t<T>
+#define nonstd_lite_in_place_index_t(K) std::in_place_index_t<K>
+
+#define nonstd_lite_in_place(T) \
+ std::in_place_t {}
+#define nonstd_lite_in_place_type(T) \
+ std::in_place_type_t<T> {}
+#define nonstd_lite_in_place_index(K) \
+ std::in_place_index_t<K> {}
+
+} // namespace nonstd
+
+#else // optional_CPP17_OR_GREATER
+
+#include <cstddef>
+
+namespace nonstd {
+namespace detail {
+
+template <class T> struct in_place_type_tag {};
+
+template <std::size_t K> struct in_place_index_tag {};
+
+} // namespace detail
+
+struct in_place_t {};
+
+template <class T>
+inline in_place_t
+in_place(detail::in_place_type_tag<T> /*unused*/ = detail::in_place_type_tag<T>()) {
+ return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t
+in_place(detail::in_place_index_tag<K> /*unused*/ = detail::in_place_index_tag<K>()) {
+ return in_place_t();
+}
+
+template <class T>
+inline in_place_t
+in_place_type(detail::in_place_type_tag<T> /*unused*/ = detail::in_place_type_tag<T>()) {
+ return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t
+in_place_index(detail::in_place_index_tag<K> /*unused*/ = detail::in_place_index_tag<K>()) {
+ return in_place_t();
+}
+
+// mimic templated typedef:
+
+#define nonstd_lite_in_place_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_type_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_index_t(K) \
+ nonstd::in_place_t (&)(nonstd::detail::in_place_index_tag<K>)
+
+#define nonstd_lite_in_place(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_type(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_index(K) nonstd::in_place_index<K>
+
+} // namespace nonstd
+
+#endif // optional_CPP17_OR_GREATER
+#endif // nonstd_lite_HAVE_IN_PLACE_TYPES
+
+//
+// Using std::optional:
+//
+
+#if optional_USES_STD_OPTIONAL
+
+#include <optional>
+
+namespace nonstd {
+
+using std::bad_optional_access;
+using std::hash;
+using std::optional;
+
+using std::nullopt;
+using std::nullopt_t;
+
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+using std::make_optional;
+using std::swap;
+} // namespace nonstd
+
+#else // optional_USES_STD_OPTIONAL
+
+#include <cassert>
+#include <utility>
+
+// optional-lite alignment configuration:
+
+#ifndef optional_CONFIG_MAX_ALIGN_HACK
+#define optional_CONFIG_MAX_ALIGN_HACK 0
+#endif
+
+#ifndef optional_CONFIG_ALIGN_AS
+// no default, used in #if defined()
+#endif
+
+#ifndef optional_CONFIG_ALIGN_AS_FALLBACK
+#define optional_CONFIG_ALIGN_AS_FALLBACK double
+#endif
+
+// Compiler warning suppression:
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundef"
+#elif defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wundef"
+#elif defined(_MSC_VER)
+#pragma warning(push)
+#endif
+
+// half-open range [lo..hi):
+#define optional_BETWEEN(v, lo, hi) ((lo) <= (v) && (v) < (hi))
+
+// Compiler versions:
+//
+// MSVC++ 6.0 _MSC_VER == 1200 optional_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0)
+// MSVC++ 7.0 _MSC_VER == 1300 optional_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002)
+// MSVC++ 7.1 _MSC_VER == 1310 optional_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003)
+// MSVC++ 8.0 _MSC_VER == 1400 optional_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005)
+// MSVC++ 9.0 _MSC_VER == 1500 optional_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008)
+// MSVC++ 10.0 _MSC_VER == 1600 optional_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010)
+// MSVC++ 11.0 _MSC_VER == 1700 optional_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012)
+// MSVC++ 12.0 _MSC_VER == 1800 optional_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013)
+// MSVC++ 14.0 _MSC_VER == 1900 optional_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015)
+// MSVC++ 14.1 _MSC_VER >= 1910 optional_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017)
+// MSVC++ 14.2 _MSC_VER >= 1920 optional_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define optional_COMPILER_MSVC_VER (_MSC_VER)
+#define optional_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * (5 + (_MSC_VER < 1900)))
+#else
+#define optional_COMPILER_MSVC_VER 0
+#define optional_COMPILER_MSVC_VERSION 0
+#endif
+
+#define optional_COMPILER_VERSION(major, minor, patch) (10 * (10 * (major) + (minor)) + (patch))
+
+#if defined(__GNUC__) && !defined(__clang__)
+#define optional_COMPILER_GNUC_VERSION \
+ optional_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+#define optional_COMPILER_GNUC_VERSION 0
+#endif
+
+#if defined(__clang__)
+#define optional_COMPILER_CLANG_VERSION \
+ optional_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+#define optional_COMPILER_CLANG_VERSION 0
+#endif
+
+#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 140)
+#pragma warning(disable : 4345) // initialization behavior changed
+#endif
+
+#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 150)
+#pragma warning(disable : 4814) // in C++14 'constexpr' will not imply 'const'
+#endif
+
+// Presence of language and library features:
+
+#define optional_HAVE(FEATURE) (optional_HAVE_##FEATURE)
+
+#ifdef _HAS_CPP0X
+#define optional_HAS_CPP0X _HAS_CPP0X
+#else
+#define optional_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for optional-lite:
+
+#if optional_COMPILER_MSVC_VER >= 1900
+#undef optional_CPP11_OR_GREATER
+#define optional_CPP11_OR_GREATER 1
+#endif
+
+#define optional_CPP11_90 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1500)
+#define optional_CPP11_100 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1600)
+#define optional_CPP11_110 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1700)
+#define optional_CPP11_120 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1800)
+#define optional_CPP11_140 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1900)
+#define optional_CPP11_141 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1910)
+
+#define optional_CPP11_140_490 \
+ ((optional_CPP11_OR_GREATER_ && optional_COMPILER_GNUC_VERSION >= 490) || \
+ (optional_COMPILER_MSVC_VER >= 1910))
+
+#define optional_CPP14_000 (optional_CPP14_OR_GREATER)
+#define optional_CPP17_000 (optional_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define optional_HAVE_CONSTEXPR_11 optional_CPP11_140
+#define optional_HAVE_IS_DEFAULT optional_CPP11_140
+#define optional_HAVE_NOEXCEPT optional_CPP11_140
+#define optional_HAVE_NULLPTR optional_CPP11_100
+#define optional_HAVE_REF_QUALIFIER optional_CPP11_140_490
+#define optional_HAVE_INITIALIZER_LIST optional_CPP11_140
+
+// Presence of C++14 language features:
+
+#define optional_HAVE_CONSTEXPR_14 optional_CPP14_000
+
+// Presence of C++17 language features:
+
+#define optional_HAVE_NODISCARD optional_CPP17_000
+
+// Presence of C++ library features:
+
+#define optional_HAVE_CONDITIONAL optional_CPP11_120
+#define optional_HAVE_REMOVE_CV optional_CPP11_120
+#define optional_HAVE_TYPE_TRAITS optional_CPP11_90
+
+#define optional_HAVE_TR1_TYPE_TRAITS (!!optional_COMPILER_GNUC_VERSION)
+#define optional_HAVE_TR1_ADD_POINTER (!!optional_COMPILER_GNUC_VERSION)
+
+// C++ feature usage:
+
+#if optional_HAVE(CONSTEXPR_11)
+#define optional_constexpr constexpr
+#else
+#define optional_constexpr /*constexpr*/
+#endif
+
+#if optional_HAVE(IS_DEFAULT)
+#define optional_is_default = default;
+#else
+#define optional_is_default \
+ {}
+#endif
+
+#if optional_HAVE(CONSTEXPR_14)
+#define optional_constexpr14 constexpr
+#else
+#define optional_constexpr14 /*constexpr*/
+#endif
+
+#if optional_HAVE(NODISCARD)
+#define optional_nodiscard [[nodiscard]]
+#else
+#define optional_nodiscard /*[[nodiscard]]*/
+#endif
+
+#if optional_HAVE(NOEXCEPT)
+#define optional_noexcept noexcept
+#else
+#define optional_noexcept /*noexcept*/
+#endif
+
+#if optional_HAVE(NULLPTR)
+#define optional_nullptr nullptr
+#else
+#define optional_nullptr NULL
+#endif
+
+#if optional_HAVE(REF_QUALIFIER)
+// NOLINTNEXTLINE( bugprone-macro-parentheses )
+#define optional_ref_qual &
+#define optional_refref_qual &&
+#else
+#define optional_ref_qual /*&*/
+#define optional_refref_qual /*&&*/
+#endif
+
+// additional includes:
+
+#if optional_CONFIG_NO_EXCEPTIONS
+// already included: <cassert>
+#else
+#include <stdexcept>
+#endif
+
+#if optional_CPP11_OR_GREATER
+#include <functional>
+#endif
+
+#if optional_HAVE(INITIALIZER_LIST)
+#include <initializer_list>
+#endif
+
+#if optional_HAVE(TYPE_TRAITS)
+#include <type_traits>
+#elif optional_HAVE(TR1_TYPE_TRAITS)
+#include <tr1/type_traits>
+#endif
+
+// Method enabling
+
+#if optional_CPP11_OR_GREATER
+
+#define optional_REQUIRES_0(...) \
+ template <bool B = (__VA_ARGS__), typename std::enable_if<B, int>::type = 0>
+
+#define optional_REQUIRES_T(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0
+
+#define optional_REQUIRES_R(R, ...) typename std::enable_if<(__VA_ARGS__), R>::type
+
+#define optional_REQUIRES_A(...) , typename std::enable_if<(__VA_ARGS__), void *>::type = nullptr
+
+#endif
+
+//
+// optional:
+//
+
+namespace nonstd {
+namespace optional_lite {
+
+namespace std11 {
+
+#if optional_CPP11_OR_GREATER
+using std::move;
+#else
+template <typename T> T &move(T &t) { return t; }
+#endif
+
+#if optional_HAVE(CONDITIONAL)
+using std::conditional;
+#else
+template <bool B, typename T, typename F> struct conditional { typedef T type; };
+template <typename T, typename F> struct conditional<false, T, F> { typedef F type; };
+#endif // optional_HAVE_CONDITIONAL
+
+// gcc < 5:
+#if optional_CPP11_OR_GREATER
+#if optional_BETWEEN(optional_COMPILER_GNUC_VERSION, 1, 500)
+template <typename T> struct is_trivially_copy_constructible : std::true_type {};
+template <typename T> struct is_trivially_move_constructible : std::true_type {};
+#else
+using std::is_trivially_copy_constructible;
+using std::is_trivially_move_constructible;
+#endif
+#endif
+} // namespace std11
+
+#if optional_CPP11_OR_GREATER
+
+/// type traits C++17:
+
+namespace std17 {
+
+#if optional_CPP17_OR_GREATER
+
+using std::is_nothrow_swappable;
+using std::is_swappable;
+
+#elif optional_CPP11_OR_GREATER
+
+namespace detail {
+
+using std::swap;
+
+struct is_swappable {
+ template <typename T, typename = decltype(swap(std::declval<T &>(), std::declval<T &>()))>
+ static std::true_type test(int /*unused*/);
+
+ template <typename> static std::false_type test(...);
+};
+
+struct is_nothrow_swappable {
+ // wrap noexcept(expr) in separate function as work-around for VC140 (VS2015):
+
+ template <typename T> static constexpr bool satisfies() {
+ return noexcept(swap(std::declval<T &>(), std::declval<T &>()));
+ }
+
+ template <typename T>
+ static auto test(int /*unused*/) -> std::integral_constant<bool, satisfies<T>()> {}
+
+ template <typename> static auto test(...) -> std::false_type;
+};
+
+} // namespace detail
+
+// is [nothow] swappable:
+
+template <typename T> struct is_swappable : decltype(detail::is_swappable::test<T>(0)) {};
+
+template <typename T>
+struct is_nothrow_swappable : decltype(detail::is_nothrow_swappable::test<T>(0)) {};
+
+#endif // optional_CPP17_OR_GREATER
+
+} // namespace std17
+
+/// type traits C++20:
+
+namespace std20 {
+
+template <typename T> struct remove_cvref {
+ typedef typename std::remove_cv<typename std::remove_reference<T>::type>::type type;
+};
+
+} // namespace std20
+
+#endif // optional_CPP11_OR_GREATER
+
+/// class optional
+
+template <typename T> class optional;
+
+namespace detail {
+
+// C++11 emulation:
+
+struct nulltype {};
+
+template <typename Head, typename Tail> struct typelist {
+ typedef Head head;
+ typedef Tail tail;
+};
+
+#if optional_CONFIG_MAX_ALIGN_HACK
+
+// Max align, use most restricted type for alignment:
+
+#define optional_UNIQUE(name) optional_UNIQUE2(name, __LINE__)
+#define optional_UNIQUE2(name, line) optional_UNIQUE3(name, line)
+#define optional_UNIQUE3(name, line) name##line
+
+#define optional_ALIGN_TYPE(type) \
+ type optional_UNIQUE(_t); \
+ struct_t<type> optional_UNIQUE(_st)
+
+template <typename T> struct struct_t { T _; };
+
+union max_align_t {
+ optional_ALIGN_TYPE(char);
+ optional_ALIGN_TYPE(short int);
+ optional_ALIGN_TYPE(int);
+ optional_ALIGN_TYPE(long int);
+ optional_ALIGN_TYPE(float);
+ optional_ALIGN_TYPE(double);
+ optional_ALIGN_TYPE(long double);
+ optional_ALIGN_TYPE(char *);
+ optional_ALIGN_TYPE(short int *);
+ optional_ALIGN_TYPE(int *);
+ optional_ALIGN_TYPE(long int *);
+ optional_ALIGN_TYPE(float *);
+ optional_ALIGN_TYPE(double *);
+ optional_ALIGN_TYPE(long double *);
+ optional_ALIGN_TYPE(void *);
+
+#ifdef HAVE_LONG_LONG
+ optional_ALIGN_TYPE(long long);
+#endif
+
+ struct Unknown;
+
+ Unknown (*optional_UNIQUE(_))(Unknown);
+ Unknown *Unknown::*optional_UNIQUE(_);
+ Unknown (Unknown::*optional_UNIQUE(_))(Unknown);
+
+ struct_t<Unknown (*)(Unknown)> optional_UNIQUE(_);
+ struct_t<Unknown * Unknown::*> optional_UNIQUE(_);
+ struct_t<Unknown (Unknown::*)(Unknown)> optional_UNIQUE(_);
+};
+
+#undef optional_UNIQUE
+#undef optional_UNIQUE2
+#undef optional_UNIQUE3
+
+#undef optional_ALIGN_TYPE
+
+#elif defined(optional_CONFIG_ALIGN_AS) // optional_CONFIG_MAX_ALIGN_HACK
+
+// Use user-specified type for alignment:
+
+#define optional_ALIGN_AS(unused) optional_CONFIG_ALIGN_AS
+
+#else // optional_CONFIG_MAX_ALIGN_HACK
+
+// Determine POD type to use for alignment:
+
+#define optional_ALIGN_AS(to_align) \
+ typename type_of_size<alignment_types, alignment_of<to_align>::value>::type
+
+template <typename T> struct alignment_of;
+
+template <typename T> struct alignment_of_hack {
+ char c;
+ T t;
+ alignment_of_hack();
+};
+
+template <size_t A, size_t S> struct alignment_logic {
+ enum { value = A < S ? A : S };
+};
+
+template <typename T> struct alignment_of {
+ enum { value = alignment_logic<sizeof(alignment_of_hack<T>) - sizeof(T), sizeof(T)>::value };
+};
+
+template <typename List, size_t N> struct type_of_size {
+ typedef
+ typename std11::conditional<N == sizeof(typename List::head), typename List::head,
+ typename type_of_size<typename List::tail, N>::type>::type type;
+};
+
+template <size_t N> struct type_of_size<nulltype, N> {
+ typedef optional_CONFIG_ALIGN_AS_FALLBACK type;
+};
+
+template <typename T> struct struct_t { T _; };
+
+#define optional_ALIGN_TYPE(type) typelist < type, typelist < struct_t<type>
+
+struct Unknown;
+
+typedef optional_ALIGN_TYPE(char), optional_ALIGN_TYPE(short), optional_ALIGN_TYPE(int),
+ optional_ALIGN_TYPE(long), optional_ALIGN_TYPE(float), optional_ALIGN_TYPE(double),
+ optional_ALIGN_TYPE(long double),
+
+ optional_ALIGN_TYPE(char *), optional_ALIGN_TYPE(short *), optional_ALIGN_TYPE(int *),
+ optional_ALIGN_TYPE(long *), optional_ALIGN_TYPE(float *), optional_ALIGN_TYPE(double *),
+ optional_ALIGN_TYPE(long double *),
+
+ optional_ALIGN_TYPE(Unknown (*)(Unknown)), optional_ALIGN_TYPE(Unknown *Unknown::*),
+ optional_ALIGN_TYPE(Unknown (Unknown::*)(Unknown)),
+
+ nulltype >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> alignment_types;
+
+#undef optional_ALIGN_TYPE
+
+#endif // optional_CONFIG_MAX_ALIGN_HACK
+
+/// C++03 constructed union to hold value.
+
+template <typename T> union storage_t {
+ // private:
+ // template< typename > friend class optional;
+
+ typedef T value_type;
+
+ storage_t() optional_is_default
+
+ explicit storage_t(value_type const &v) {
+ construct_value(v);
+ }
+
+ void construct_value(value_type const &v) { ::new (value_ptr()) value_type(v); }
+
+#if optional_CPP11_OR_GREATER
+
+ explicit storage_t(value_type &&v) { construct_value(std::move(v)); }
+
+ void construct_value(value_type &&v) { ::new (value_ptr()) value_type(std::move(v)); }
+
+ template <class... Args> void emplace(Args &&... args) {
+ ::new (value_ptr()) value_type(std::forward<Args>(args)...);
+ }
+
+ template <class U, class... Args> void emplace(std::initializer_list<U> il, Args &&... args) {
+ ::new (value_ptr()) value_type(il, std::forward<Args>(args)...);
+ }
+
+#endif
+
+ void destruct_value() { value_ptr()->~T(); }
+
+ optional_nodiscard value_type const *value_ptr() const { return as<value_type>(); }
+
+ value_type *value_ptr() { return as<value_type>(); }
+
+ optional_nodiscard value_type const &value() const optional_ref_qual { return *value_ptr(); }
+
+ value_type &value() optional_ref_qual { return *value_ptr(); }
+
+#if optional_HAVE(REF_QUALIFIER)
+
+ optional_nodiscard value_type const &&value() const optional_refref_qual {
+ return std::move(value());
+ }
+
+ value_type &&value() optional_refref_qual { return std::move(value()); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+
+ using aligned_storage_t =
+ typename std::aligned_storage<sizeof(value_type), alignof(value_type)>::type;
+ aligned_storage_t data;
+
+#elif optional_CONFIG_MAX_ALIGN_HACK
+
+ typedef struct {
+ unsigned char data[sizeof(value_type)];
+ } aligned_storage_t;
+
+ max_align_t hack;
+ aligned_storage_t data;
+
+#else
+ typedef optional_ALIGN_AS(value_type) align_as_type;
+
+ typedef struct {
+ align_as_type data[1 + (sizeof(value_type) - 1) / sizeof(align_as_type)];
+ } aligned_storage_t;
+ aligned_storage_t data;
+
+#undef optional_ALIGN_AS
+
+#endif // optional_CONFIG_MAX_ALIGN_HACK
+
+ optional_nodiscard void *ptr() optional_noexcept { return &data; }
+
+ optional_nodiscard void const *ptr() const optional_noexcept { return &data; }
+
+ template <typename U> optional_nodiscard U *as() { return reinterpret_cast<U *>(ptr()); }
+
+ template <typename U> optional_nodiscard U const *as() const {
+ return reinterpret_cast<U const *>(ptr());
+ }
+};
+
+} // namespace detail
+
+/// disengaged state tag
+
+struct nullopt_t {
+ struct init {};
+ explicit optional_constexpr nullopt_t(init /*unused*/) optional_noexcept {}
+};
+
+#if optional_HAVE(CONSTEXPR_11)
+constexpr nullopt_t nullopt{nullopt_t::init{}};
+#else
+// extra parenthesis to prevent the most vexing parse:
+const nullopt_t nullopt((nullopt_t::init()));
+#endif
+
+/// optional access error
+
+#if !optional_CONFIG_NO_EXCEPTIONS
+
+class bad_optional_access : public std::logic_error {
+public:
+ explicit bad_optional_access() : logic_error("bad optional access") {}
+};
+
+#endif // optional_CONFIG_NO_EXCEPTIONS
+
+/// optional
+
+template <typename T> class optional {
+private:
+ template <typename> friend class optional;
+
+ typedef void (optional::*safe_bool)() const;
+
+public:
+ typedef T value_type;
+
+ // x.x.3.1, constructors
+
+ // 1a - default construct
+ optional_constexpr optional() optional_noexcept : has_value_(false), contained() {}
+
+ // 1b - construct explicitly empty
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ optional_constexpr optional(nullopt_t /*unused*/) optional_noexcept : has_value_(false),
+ contained() {}
+
+ // 2 - copy-construct
+#if optional_CPP11_OR_GREATER
+ // template< typename U = T
+ // optional_REQUIRES_T(
+ // std::is_copy_constructible<U>::value
+ // || std11::is_trivially_copy_constructible<U>::value
+ // )
+ // >
+#endif
+ optional_constexpr14 optional(optional const &other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(other.contained.value());
+ }
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 3 (C++11) - move-construct from optional
+ template <typename U = T optional_REQUIRES_T(std::is_move_constructible<U>::value ||
+ std11::is_trivially_move_constructible<U>::value)>
+ optional_constexpr14 optional(optional &&other)
+ // NOLINTNEXTLINE( performance-noexcept-move-constructor )
+ noexcept(std::is_nothrow_move_constructible<T>::value)
+ : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(std::move(other.contained.value()));
+ }
+ }
+
+ // 4a (C++11) - explicit converting copy-construct from optional
+ template <typename U optional_REQUIRES_T(std::is_constructible<T, U const &>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ !std::is_convertible<U const &, T>::value /*=> explicit
+ */
+ )>
+ explicit optional(optional<U> const &other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(T{other.contained.value()});
+ }
+ }
+#endif // optional_CPP11_OR_GREATER
+
+ // 4b (C++98 and later) - non-explicit converting copy-construct from optional
+ template <typename U
+#if optional_CPP11_OR_GREATER
+ optional_REQUIRES_T(std::is_constructible<T, U const &>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ std::is_convertible<U const &, T>::value /*=> non-explicit */
+ )
+#endif // optional_CPP11_OR_GREATER
+ >
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ /*non-explicit*/ optional(optional<U> const &other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(other.contained.value());
+ }
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 5a (C++11) - explicit converting move-construct from optional
+ template <typename U optional_REQUIRES_T(std::is_constructible<T, U &&>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ !std::is_convertible<U &&, T>::value /*=> explicit */
+ )>
+ explicit optional(optional<U> &&other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(T{std::move(other.contained.value())});
+ }
+ }
+
+ // 5a (C++11) - non-explicit converting move-construct from optional
+ template <typename U optional_REQUIRES_T(std::is_constructible<T, U &&>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ std::is_convertible<U &&, T>::value /*=> non-explicit */
+ )>
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ /*non-explicit*/ optional(optional<U> &&other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(std::move(other.contained.value()));
+ }
+ }
+
+ // 6 (C++11) - in-place construct
+ template <typename... Args optional_REQUIRES_T(std::is_constructible<T, Args &&...>::value)>
+ optional_constexpr explicit optional(nonstd_lite_in_place_t(T), Args &&... args)
+ : has_value_(true), contained(T(std::forward<Args>(args)...)) {}
+
+ // 7 (C++11) - in-place construct, initializer-list
+ template <typename U,
+ typename... Args optional_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value)>
+ optional_constexpr explicit optional(nonstd_lite_in_place_t(T), std::initializer_list<U> il,
+ Args &&... args)
+ : has_value_(true), contained(T(il, std::forward<Args>(args)...)) {}
+
+ // 8a (C++11) - explicit move construct from value
+ template <
+ typename U = T optional_REQUIRES_T(
+ std::is_constructible<T, U &&>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+ !std::is_convertible<U &&, T>::value /*=> explicit */
+ )>
+ optional_constexpr explicit optional(U &&value)
+ : has_value_(true), contained(T{std::forward<U>(value)}) {}
+
+ // 8b (C++11) - non-explicit move construct from value
+ template <
+ typename U = T optional_REQUIRES_T(
+ std::is_constructible<T, U &&>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+ std::is_convertible<U &&, T>::value /*=> non-explicit */
+ )>
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ optional_constexpr /*non-explicit*/ optional(U &&value)
+ : has_value_(true), contained(std::forward<U>(value)) {}
+
+#else // optional_CPP11_OR_GREATER
+
+ // 8 (C++98)
+ optional(value_type const &value) : has_value_(true), contained(value) {}
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.2, destructor
+
+ ~optional() {
+ if (has_value()) {
+ contained.destruct_value();
+ }
+ }
+
+ // x.x.3.3, assignment
+
+ // 1 (C++98and later) - assign explicitly empty
+ optional &operator=(nullopt_t /*unused*/) optional_noexcept {
+ reset();
+ return *this;
+ }
+
+ // 2 (C++98and later) - copy-assign from optional
+#if optional_CPP11_OR_GREATER
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(optional &, true
+ // std::is_copy_constructible<T>::value
+ // && std::is_copy_assignable<T>::value
+ )
+ operator=(optional const &other) noexcept(
+ std::is_nothrow_move_assignable<T>::value &&std::is_nothrow_move_constructible<T>::value)
+#else
+ optional &operator=(optional const &other)
+#endif
+ {
+ if ((has_value() == true) && (other.has_value() == false)) {
+ reset();
+ } else if ((has_value() == false) && (other.has_value() == true)) {
+ initialize(*other);
+ } else if ((has_value() == true) && (other.has_value() == true)) {
+ contained.value() = *other;
+ }
+ return *this;
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 3 (C++11) - move-assign from optional
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(optional &, true
+ // std::is_move_constructible<T>::value
+ // && std::is_move_assignable<T>::value
+ )
+ operator=(optional &&other) noexcept {
+ if ((has_value() == true) && (other.has_value() == false)) {
+ reset();
+ } else if ((has_value() == false) && (other.has_value() == true)) {
+ initialize(std::move(*other));
+ } else if ((has_value() == true) && (other.has_value() == true)) {
+ contained.value() = std::move(*other);
+ }
+ return *this;
+ }
+
+ // 4 (C++11) - move-assign from value
+ template <typename U = T>
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional &,
+ std::is_constructible<T, U>::value &&std::is_assignable<T &, U>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+ !(std::is_scalar<T>::value && std::is_same<T, typename std::decay<U>::type>::value))
+ operator=(U &&value) {
+ if (has_value()) {
+ contained.value() = std::forward<U>(value);
+ } else {
+ initialize(T(std::forward<U>(value)));
+ }
+ return *this;
+ }
+
+#else // optional_CPP11_OR_GREATER
+
+ // 4 (C++98) - copy-assign from value
+ template <typename U /*= T*/> optional &operator=(U const &value) {
+ if (has_value())
+ contained.value() = value;
+ else
+ initialize(T(value));
+ return *this;
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // 5 (C++98 and later) - converting copy-assign from optional
+ template <typename U>
+#if optional_CPP11_OR_GREATER
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional &,
+ std::is_constructible<T, U const &>::value &&std::is_assignable<T &, U const &>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ !std::is_assignable<T &, optional<U> &>::value &&
+ !std::is_assignable<T &, optional<U> &&>::value &&
+ !std::is_assignable<T &, optional<U> const &>::value &&
+ !std::is_assignable<T &, optional<U> const &&>::value)
+#else
+ optional &
+#endif // optional_CPP11_OR_GREATER
+ operator=(optional<U> const &other) {
+ return *this = optional(other);
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 6 (C++11) - converting move-assign from optional
+ template <typename U>
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(optional &,
+ std::is_constructible<T, U>::value &&std::is_assignable<T &, U>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ !std::is_assignable<T &, optional<U> &>::value &&
+ !std::is_assignable<T &, optional<U> &&>::value &&
+ !std::is_assignable<T &, optional<U> const &>::value &&
+ !std::is_assignable<T &, optional<U> const &&>::value)
+ operator=(optional<U> &&other) {
+ return *this = optional(std::move(other));
+ }
+
+ // 7 (C++11) - emplace
+ template <typename... Args optional_REQUIRES_T(std::is_constructible<T, Args &&...>::value)>
+ T &emplace(Args &&... args) {
+ *this = nullopt;
+ contained.emplace(std::forward<Args>(args)...);
+ has_value_ = true;
+ return contained.value();
+ }
+
+ // 8 (C++11) - emplace, initializer-list
+ template <typename U,
+ typename... Args optional_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value)>
+ T &emplace(std::initializer_list<U> il, Args &&... args) {
+ *this = nullopt;
+ contained.emplace(il, std::forward<Args>(args)...);
+ has_value_ = true;
+ return contained.value();
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.4, swap
+
+ void swap(optional &other)
+#if optional_CPP11_OR_GREATER
+ noexcept(std::is_nothrow_move_constructible<T>::value &&std17::is_nothrow_swappable<T>::value)
+#endif
+ {
+ using std::swap;
+ if ((has_value() == true) && (other.has_value() == true)) {
+ swap(**this, *other);
+ } else if ((has_value() == false) && (other.has_value() == true)) {
+ initialize(std11::move(*other));
+ other.reset();
+ } else if ((has_value() == true) && (other.has_value() == false)) {
+ other.initialize(std11::move(**this));
+ reset();
+ }
+ }
+
+ // x.x.3.5, observers
+
+ optional_constexpr value_type const *operator->() const {
+ return assert(has_value()), contained.value_ptr();
+ }
+
+ optional_constexpr14 value_type *operator->() {
+ return assert(has_value()), contained.value_ptr();
+ }
+
+ optional_constexpr value_type const &operator*() const optional_ref_qual {
+ return assert(has_value()), contained.value();
+ }
+
+ optional_constexpr14 value_type &operator*() optional_ref_qual {
+ return assert(has_value()), contained.value();
+ }
+
+#if optional_HAVE(REF_QUALIFIER)
+
+ optional_constexpr value_type const &&operator*() const optional_refref_qual {
+ return std::move(**this);
+ }
+
+ optional_constexpr14 value_type &&operator*() optional_refref_qual { return std::move(**this); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+ optional_constexpr explicit operator bool() const optional_noexcept { return has_value(); }
+#else
+ optional_constexpr operator safe_bool() const optional_noexcept {
+ return has_value() ? &optional::this_type_does_not_support_comparisons : 0;
+ }
+#endif
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr bool has_value() const optional_noexcept {
+ return has_value_;
+ }
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr14 value_type const &value() const optional_ref_qual {
+#if optional_CONFIG_NO_EXCEPTIONS
+ assert(has_value());
+#else
+ if (!has_value()) {
+ throw bad_optional_access();
+ }
+#endif
+ return contained.value();
+ }
+
+ optional_constexpr14 value_type &value() optional_ref_qual {
+#if optional_CONFIG_NO_EXCEPTIONS
+ assert(has_value());
+#else
+ if (!has_value()) {
+ throw bad_optional_access();
+ }
+#endif
+ return contained.value();
+ }
+
+#if optional_HAVE(REF_QUALIFIER) && \
+ (!optional_COMPILER_GNUC_VERSION || optional_COMPILER_GNUC_VERSION >= 490)
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr value_type const &&value() const optional_refref_qual {
+ return std::move(value());
+ }
+
+ optional_constexpr14 value_type &&value() optional_refref_qual { return std::move(value()); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+
+ template <typename U> optional_constexpr value_type value_or(U &&v) const optional_ref_qual {
+ return has_value() ? contained.value() : static_cast<T>(std::forward<U>(v));
+ }
+
+ template <typename U> optional_constexpr14 value_type value_or(U &&v) optional_refref_qual {
+ return has_value() ? std::move(contained.value()) : static_cast<T>(std::forward<U>(v));
+ }
+
+#else
+
+ template <typename U> optional_constexpr value_type value_or(U const &v) const {
+ return has_value() ? contained.value() : static_cast<value_type>(v);
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.6, modifiers
+
+ void reset() optional_noexcept {
+ if (has_value()) {
+ contained.destruct_value();
+ }
+
+ has_value_ = false;
+ }
+
+private:
+ void this_type_does_not_support_comparisons() const {}
+
+ template <typename V> void initialize(V const &value) {
+ assert(!has_value());
+ contained.construct_value(value);
+ has_value_ = true;
+ }
+
+#if optional_CPP11_OR_GREATER
+ template <typename V> void initialize(V &&value) {
+ assert(!has_value());
+ contained.construct_value(std::move(value));
+ has_value_ = true;
+ }
+
+#endif
+
+private:
+ bool has_value_;
+ detail::storage_t<value_type> contained;
+};
+
+// Relational operators
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(optional<T> const &x, optional<U> const &y) {
+ return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(optional<T> const &x, optional<U> const &y) {
+ return !(x == y);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(optional<T> const &x, optional<U> const &y) {
+ return (!y) ? false : (!x) ? true : *x < *y;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(optional<T> const &x, optional<U> const &y) {
+ return (y < x);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(optional<T> const &x, optional<U> const &y) {
+ return !(y < x);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(optional<T> const &x, optional<U> const &y) {
+ return !(x < y);
+}
+
+// Comparison with nullopt
+
+template <typename T>
+inline optional_constexpr bool operator==(optional<T> const &x,
+ nullopt_t /*unused*/) optional_noexcept {
+ return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator==(nullopt_t /*unused*/,
+ optional<T> const &x) optional_noexcept {
+ return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator!=(optional<T> const &x,
+ nullopt_t /*unused*/) optional_noexcept {
+ return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator!=(nullopt_t /*unused*/,
+ optional<T> const &x) optional_noexcept {
+ return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<(optional<T> const & /*unused*/,
+ nullopt_t /*unused*/) optional_noexcept {
+ return false;
+}
+
+template <typename T>
+inline optional_constexpr bool operator<(nullopt_t /*unused*/,
+ optional<T> const &x) optional_noexcept {
+ return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<=(optional<T> const &x,
+ nullopt_t /*unused*/) optional_noexcept {
+ return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<=(nullopt_t /*unused*/,
+ optional<T> const & /*unused*/) optional_noexcept {
+ return true;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>(optional<T> const &x,
+ nullopt_t /*unused*/) optional_noexcept {
+ return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator>(nullopt_t /*unused*/,
+ optional<T> const & /*unused*/) optional_noexcept {
+ return false;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>=(optional<T> const & /*unused*/,
+ nullopt_t /*unused*/) optional_noexcept {
+ return true;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>=(nullopt_t /*unused*/,
+ optional<T> const &x) optional_noexcept {
+ return (!x);
+}
+
+// Comparison with T
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(optional<T> const &x, U const &v) {
+ return bool(x) ? *x == v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(U const &v, optional<T> const &x) {
+ return bool(x) ? v == *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(optional<T> const &x, U const &v) {
+ return bool(x) ? *x != v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(U const &v, optional<T> const &x) {
+ return bool(x) ? v != *x : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(optional<T> const &x, U const &v) {
+ return bool(x) ? *x < v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(U const &v, optional<T> const &x) {
+ return bool(x) ? v < *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(optional<T> const &x, U const &v) {
+ return bool(x) ? *x <= v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(U const &v, optional<T> const &x) {
+ return bool(x) ? v <= *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(optional<T> const &x, U const &v) {
+ return bool(x) ? *x > v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(U const &v, optional<T> const &x) {
+ return bool(x) ? v > *x : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(optional<T> const &x, U const &v) {
+ return bool(x) ? *x >= v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(U const &v, optional<T> const &x) {
+ return bool(x) ? v >= *x : true;
+}
+
+// Specialized algorithms
+
+template <
+ typename T
+#if optional_CPP11_OR_GREATER
+ optional_REQUIRES_T(std::is_move_constructible<T>::value &&std17::is_swappable<T>::value)
+#endif
+ >
+void swap(optional<T> &x, optional<T> &y)
+#if optional_CPP11_OR_GREATER
+ noexcept(noexcept(x.swap(y)))
+#endif
+{
+ x.swap(y);
+}
+
+#if optional_CPP11_OR_GREATER
+
+template <typename T>
+optional_constexpr optional<typename std::decay<T>::type> make_optional(T &&value) {
+ return optional<typename std::decay<T>::type>(std::forward<T>(value));
+}
+
+template <typename T, typename... Args>
+optional_constexpr optional<T> make_optional(Args &&... args) {
+ return optional<T>(nonstd_lite_in_place(T), std::forward<Args>(args)...);
+}
+
+template <typename T, typename U, typename... Args>
+optional_constexpr optional<T> make_optional(std::initializer_list<U> il, Args &&... args) {
+ return optional<T>(nonstd_lite_in_place(T), il, std::forward<Args>(args)...);
+}
+
+#else
+
+template <typename T> optional<T> make_optional(T const &value) { return optional<T>(value); }
+
+#endif // optional_CPP11_OR_GREATER
+
+} // namespace optional_lite
+
+using optional_lite::nullopt;
+using optional_lite::nullopt_t;
+using optional_lite::optional;
+
+#if !optional_CONFIG_NO_EXCEPTIONS
+using optional_lite::bad_optional_access;
+#endif
+
+using optional_lite::make_optional;
+
+} // namespace nonstd
+
+#if optional_CPP11_OR_GREATER
+
+// specialize the std::hash algorithm:
+
+namespace std {
+
+template <class T> struct hash<nonstd::optional<T>> {
+public:
+ std::size_t operator()(nonstd::optional<T> const &v) const optional_noexcept {
+ return bool(v) ? std::hash<T>{}(*v) : 0;
+ }
+};
+
+} // namespace std
+
+#endif // optional_CPP11_OR_GREATER
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#elif defined(__GNUC__)
+#pragma GCC diagnostic pop
+#elif defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+#endif // optional_USES_STD_OPTIONAL
+
+#endif // NONSTD_OPTIONAL_LITE_HPP
+// Copyright 2017-2020 by Martin Moene
+//
+// string-view lite, a C++17-like string_view for C++98 and later.
+// For more information see https://github.com/martinmoene/string-view-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_SV_LITE_H_INCLUDED
+#define NONSTD_SV_LITE_H_INCLUDED
+
+#define string_view_lite_MAJOR 1
+#define string_view_lite_MINOR 6
+#define string_view_lite_PATCH 0
+
+#define string_view_lite_VERSION nssv_STRINGIFY(string_view_lite_MAJOR) "." nssv_STRINGIFY(string_view_lite_MINOR) "." nssv_STRINGIFY(string_view_lite_PATCH)
+
+#define nssv_STRINGIFY( x ) nssv_STRINGIFY_( x )
+#define nssv_STRINGIFY_( x ) #x
+
+// string-view lite configuration:
+
+#define nssv_STRING_VIEW_DEFAULT 0
+#define nssv_STRING_VIEW_NONSTD 1
+#define nssv_STRING_VIEW_STD 2
+
+// tweak header support:
+
+#ifdef __has_include
+# if __has_include(<nonstd/string_view.tweak.hpp>)
+# include <nonstd/string_view.tweak.hpp>
+# endif
+#define nssv_HAVE_TWEAK_HEADER 1
+#else
+#define nssv_HAVE_TWEAK_HEADER 0
+//# pragma message("string_view.hpp: Note: Tweak header not supported.")
+#endif
+
+// string_view selection and configuration:
+
+#if !defined( nssv_CONFIG_SELECT_STRING_VIEW )
+# define nssv_CONFIG_SELECT_STRING_VIEW ( nssv_HAVE_STD_STRING_VIEW ? nssv_STRING_VIEW_STD : nssv_STRING_VIEW_NONSTD )
+#endif
+
+#ifndef nssv_CONFIG_STD_SV_OPERATOR
+# define nssv_CONFIG_STD_SV_OPERATOR 0
+#endif
+
+#ifndef nssv_CONFIG_USR_SV_OPERATOR
+# define nssv_CONFIG_USR_SV_OPERATOR 1
+#endif
+
+#ifdef nssv_CONFIG_CONVERSION_STD_STRING
+# define nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS nssv_CONFIG_CONVERSION_STD_STRING
+# define nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS nssv_CONFIG_CONVERSION_STD_STRING
+#endif
+
+#ifndef nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+# define nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS 1
+#endif
+
+#ifndef nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+# define nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS 1
+#endif
+
+#ifndef nssv_CONFIG_NO_STREAM_INSERTION
+# define nssv_CONFIG_NO_STREAM_INSERTION 0
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef nssv_CONFIG_NO_EXCEPTIONS
+# if defined(_MSC_VER)
+# include <cstddef> // for _HAS_EXCEPTIONS
+# endif
+# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS)
+# define nssv_CONFIG_NO_EXCEPTIONS 0
+# else
+# define nssv_CONFIG_NO_EXCEPTIONS 1
+# endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef nssv_CPLUSPLUS
+# if defined(_MSVC_LANG ) && !defined(__clang__)
+# define nssv_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG )
+# else
+# define nssv_CPLUSPLUS __cplusplus
+# endif
+#endif
+
+#define nssv_CPP98_OR_GREATER ( nssv_CPLUSPLUS >= 199711L )
+#define nssv_CPP11_OR_GREATER ( nssv_CPLUSPLUS >= 201103L )
+#define nssv_CPP11_OR_GREATER_ ( nssv_CPLUSPLUS >= 201103L )
+#define nssv_CPP14_OR_GREATER ( nssv_CPLUSPLUS >= 201402L )
+#define nssv_CPP17_OR_GREATER ( nssv_CPLUSPLUS >= 201703L )
+#define nssv_CPP20_OR_GREATER ( nssv_CPLUSPLUS >= 202000L )
+
+// use C++17 std::string_view if available and requested:
+
+#if nssv_CPP17_OR_GREATER && defined(__has_include )
+# if __has_include( <string_view> )
+# define nssv_HAVE_STD_STRING_VIEW 1
+# else
+# define nssv_HAVE_STD_STRING_VIEW 0
+# endif
+#else
+# define nssv_HAVE_STD_STRING_VIEW 0
+#endif
+
+#define nssv_USES_STD_STRING_VIEW ( (nssv_CONFIG_SELECT_STRING_VIEW == nssv_STRING_VIEW_STD) || ((nssv_CONFIG_SELECT_STRING_VIEW == nssv_STRING_VIEW_DEFAULT) && nssv_HAVE_STD_STRING_VIEW) )
+
+#define nssv_HAVE_STARTS_WITH ( nssv_CPP20_OR_GREATER || !nssv_USES_STD_STRING_VIEW )
+#define nssv_HAVE_ENDS_WITH nssv_HAVE_STARTS_WITH
+
+//
+// Use C++17 std::string_view:
+//
+
+#if nssv_USES_STD_STRING_VIEW
+
+#include <string_view>
+
+// Extensions for std::string:
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+
+template< class CharT, class Traits, class Allocator = std::allocator<CharT> >
+std::basic_string<CharT, Traits, Allocator>
+to_string( std::basic_string_view<CharT, Traits> v, Allocator const & a = Allocator() )
+{
+ return std::basic_string<CharT,Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+template< class CharT, class Traits, class Allocator >
+std::basic_string_view<CharT, Traits>
+to_string_view( std::basic_string<CharT, Traits, Allocator> const & s )
+{
+ return std::basic_string_view<CharT, Traits>( s.data(), s.size() );
+}
+
+// Literal operators sv and _sv:
+
+#if nssv_CONFIG_STD_SV_OPERATOR
+
+using namespace std::literals::string_view_literals;
+
+#endif
+
+#if nssv_CONFIG_USR_SV_OPERATOR
+
+inline namespace literals {
+inline namespace string_view_literals {
+
+
+constexpr std::string_view operator "" _sv( const char* str, size_t len ) noexcept // (1)
+{
+ return std::string_view{ str, len };
+}
+
+constexpr std::u16string_view operator "" _sv( const char16_t* str, size_t len ) noexcept // (2)
+{
+ return std::u16string_view{ str, len };
+}
+
+constexpr std::u32string_view operator "" _sv( const char32_t* str, size_t len ) noexcept // (3)
+{
+ return std::u32string_view{ str, len };
+}
+
+constexpr std::wstring_view operator "" _sv( const wchar_t* str, size_t len ) noexcept // (4)
+{
+ return std::wstring_view{ str, len };
+}
+
+}} // namespace literals::string_view_literals
+
+#endif // nssv_CONFIG_USR_SV_OPERATOR
+
+} // namespace nonstd
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+
+using std::string_view;
+using std::wstring_view;
+using std::u16string_view;
+using std::u32string_view;
+using std::basic_string_view;
+
+// literal "sv" and "_sv", see above
+
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+
+using std::operator<<;
+
+} // namespace nonstd
+
+#else // nssv_HAVE_STD_STRING_VIEW
+
+//
+// Before C++17: use string_view lite:
+//
+
+// Compiler versions:
+//
+// MSVC++ 6.0 _MSC_VER == 1200 nssv_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0)
+// MSVC++ 7.0 _MSC_VER == 1300 nssv_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002)
+// MSVC++ 7.1 _MSC_VER == 1310 nssv_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003)
+// MSVC++ 8.0 _MSC_VER == 1400 nssv_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005)
+// MSVC++ 9.0 _MSC_VER == 1500 nssv_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008)
+// MSVC++ 10.0 _MSC_VER == 1600 nssv_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010)
+// MSVC++ 11.0 _MSC_VER == 1700 nssv_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012)
+// MSVC++ 12.0 _MSC_VER == 1800 nssv_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013)
+// MSVC++ 14.0 _MSC_VER == 1900 nssv_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015)
+// MSVC++ 14.1 _MSC_VER >= 1910 nssv_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017)
+// MSVC++ 14.2 _MSC_VER >= 1920 nssv_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019)
+
+#if defined(_MSC_VER ) && !defined(__clang__)
+# define nssv_COMPILER_MSVC_VER (_MSC_VER )
+# define nssv_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900 ) ) )
+#else
+# define nssv_COMPILER_MSVC_VER 0
+# define nssv_COMPILER_MSVC_VERSION 0
+#endif
+
+#define nssv_COMPILER_VERSION( major, minor, patch ) ( 10 * ( 10 * (major) + (minor) ) + (patch) )
+
+#if defined( __apple_build_version__ )
+# define nssv_COMPILER_APPLECLANG_VERSION nssv_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+# define nssv_COMPILER_CLANG_VERSION 0
+#elif defined( __clang__ )
+# define nssv_COMPILER_APPLECLANG_VERSION 0
+# define nssv_COMPILER_CLANG_VERSION nssv_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+# define nssv_COMPILER_APPLECLANG_VERSION 0
+# define nssv_COMPILER_CLANG_VERSION 0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__)
+# define nssv_COMPILER_GNUC_VERSION nssv_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+# define nssv_COMPILER_GNUC_VERSION 0
+#endif
+
+// half-open range [lo..hi):
+#define nssv_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) )
+
+// Presence of language and library features:
+
+#ifdef _HAS_CPP0X
+# define nssv_HAS_CPP0X _HAS_CPP0X
+#else
+# define nssv_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for variant-lite:
+
+#if nssv_COMPILER_MSVC_VER >= 1900
+# undef nssv_CPP11_OR_GREATER
+# define nssv_CPP11_OR_GREATER 1
+#endif
+
+#define nssv_CPP11_90 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1500)
+#define nssv_CPP11_100 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1600)
+#define nssv_CPP11_110 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1700)
+#define nssv_CPP11_120 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1800)
+#define nssv_CPP11_140 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1900)
+#define nssv_CPP11_141 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1910)
+
+#define nssv_CPP14_000 (nssv_CPP14_OR_GREATER)
+#define nssv_CPP17_000 (nssv_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define nssv_HAVE_CONSTEXPR_11 nssv_CPP11_140
+#define nssv_HAVE_EXPLICIT_CONVERSION nssv_CPP11_140
+#define nssv_HAVE_INLINE_NAMESPACE nssv_CPP11_140
+#define nssv_HAVE_NOEXCEPT nssv_CPP11_140
+#define nssv_HAVE_NULLPTR nssv_CPP11_100
+#define nssv_HAVE_REF_QUALIFIER nssv_CPP11_140
+#define nssv_HAVE_UNICODE_LITERALS nssv_CPP11_140
+#define nssv_HAVE_USER_DEFINED_LITERALS nssv_CPP11_140
+#define nssv_HAVE_WCHAR16_T nssv_CPP11_100
+#define nssv_HAVE_WCHAR32_T nssv_CPP11_100
+
+#if ! ( ( nssv_CPP11_OR_GREATER && nssv_COMPILER_CLANG_VERSION ) || nssv_BETWEEN( nssv_COMPILER_CLANG_VERSION, 300, 400 ) )
+# define nssv_HAVE_STD_DEFINED_LITERALS nssv_CPP11_140
+#else
+# define nssv_HAVE_STD_DEFINED_LITERALS 0
+#endif
+
+// Presence of C++14 language features:
+
+#define nssv_HAVE_CONSTEXPR_14 nssv_CPP14_000
+
+// Presence of C++17 language features:
+
+#define nssv_HAVE_NODISCARD nssv_CPP17_000
+
+// Presence of C++ library features:
+
+#define nssv_HAVE_STD_HASH nssv_CPP11_120
+
+// Presence of compiler intrinsics:
+
+// Providing char-type specializations for compare() and length() that
+// use compiler intrinsics can improve compile- and run-time performance.
+//
+// The challenge is in using the right combinations of builtin availability
+// and its constexpr-ness.
+//
+// | compiler | __builtin_memcmp (constexpr) | memcmp (constexpr) |
+// |----------|------------------------------|---------------------|
+// | clang | 4.0 (>= 4.0 ) | any (? ) |
+// | clang-a | 9.0 (>= 9.0 ) | any (? ) |
+// | gcc | any (constexpr) | any (? ) |
+// | msvc | >= 14.2 C++17 (>= 14.2 ) | any (? ) |
+
+#define nssv_HAVE_BUILTIN_VER ( (nssv_CPP17_000 && nssv_COMPILER_MSVC_VERSION >= 142) || nssv_COMPILER_GNUC_VERSION > 0 || nssv_COMPILER_CLANG_VERSION >= 400 || nssv_COMPILER_APPLECLANG_VERSION >= 900 )
+#define nssv_HAVE_BUILTIN_CE ( nssv_HAVE_BUILTIN_VER )
+
+#define nssv_HAVE_BUILTIN_MEMCMP ( (nssv_HAVE_CONSTEXPR_14 && nssv_HAVE_BUILTIN_CE) || !nssv_HAVE_CONSTEXPR_14 )
+#define nssv_HAVE_BUILTIN_STRLEN ( (nssv_HAVE_CONSTEXPR_11 && nssv_HAVE_BUILTIN_CE) || !nssv_HAVE_CONSTEXPR_11 )
+
+#ifdef __has_builtin
+# define nssv_HAVE_BUILTIN( x ) __has_builtin( x )
+#else
+# define nssv_HAVE_BUILTIN( x ) 0
+#endif
+
+#if nssv_HAVE_BUILTIN(__builtin_memcmp) || nssv_HAVE_BUILTIN_VER
+# define nssv_BUILTIN_MEMCMP __builtin_memcmp
+#else
+# define nssv_BUILTIN_MEMCMP memcmp
+#endif
+
+#if nssv_HAVE_BUILTIN(__builtin_strlen) || nssv_HAVE_BUILTIN_VER
+# define nssv_BUILTIN_STRLEN __builtin_strlen
+#else
+# define nssv_BUILTIN_STRLEN strlen
+#endif
+
+// C++ feature usage:
+
+#if nssv_HAVE_CONSTEXPR_11
+# define nssv_constexpr constexpr
+#else
+# define nssv_constexpr /*constexpr*/
+#endif
+
+#if nssv_HAVE_CONSTEXPR_14
+# define nssv_constexpr14 constexpr
+#else
+# define nssv_constexpr14 /*constexpr*/
+#endif
+
+#if nssv_HAVE_EXPLICIT_CONVERSION
+# define nssv_explicit explicit
+#else
+# define nssv_explicit /*explicit*/
+#endif
+
+#if nssv_HAVE_INLINE_NAMESPACE
+# define nssv_inline_ns inline
+#else
+# define nssv_inline_ns /*inline*/
+#endif
+
+#if nssv_HAVE_NOEXCEPT
+# define nssv_noexcept noexcept
+#else
+# define nssv_noexcept /*noexcept*/
+#endif
+
+//#if nssv_HAVE_REF_QUALIFIER
+//# define nssv_ref_qual &
+//# define nssv_refref_qual &&
+//#else
+//# define nssv_ref_qual /*&*/
+//# define nssv_refref_qual /*&&*/
+//#endif
+
+#if nssv_HAVE_NULLPTR
+# define nssv_nullptr nullptr
+#else
+# define nssv_nullptr NULL
+#endif
+
+#if nssv_HAVE_NODISCARD
+# define nssv_nodiscard [[nodiscard]]
+#else
+# define nssv_nodiscard /*[[nodiscard]]*/
+#endif
+
+// Additional includes:
+
+#include <algorithm>
+#include <cassert>
+#include <iterator>
+#include <limits>
+#include <string> // std::char_traits<>
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+# include <ostream>
+#endif
+
+#if ! nssv_CONFIG_NO_EXCEPTIONS
+# include <stdexcept>
+#endif
+
+#if nssv_CPP11_OR_GREATER
+# include <type_traits>
+#endif
+
+// Clang, GNUC, MSVC warning suppression macros:
+
+#if defined(__clang__)
+# pragma clang diagnostic ignored "-Wreserved-user-defined-literal"
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wuser-defined-literals"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wliteral-suffix"
+#endif // __clang__
+
+#if nssv_COMPILER_MSVC_VERSION >= 140
+# define nssv_SUPPRESS_MSGSL_WARNING(expr) [[gsl::suppress(expr)]]
+# define nssv_SUPPRESS_MSVC_WARNING(code, descr) __pragma(warning(suppress: code) )
+# define nssv_DISABLE_MSVC_WARNINGS(codes) __pragma(warning(push)) __pragma(warning(disable: codes))
+#else
+# define nssv_SUPPRESS_MSGSL_WARNING(expr)
+# define nssv_SUPPRESS_MSVC_WARNING(code, descr)
+# define nssv_DISABLE_MSVC_WARNINGS(codes)
+#endif
+
+#if defined(__clang__)
+# define nssv_RESTORE_WARNINGS() _Pragma("clang diagnostic pop")
+#elif defined(__GNUC__)
+# define nssv_RESTORE_WARNINGS() _Pragma("GCC diagnostic pop")
+#elif nssv_COMPILER_MSVC_VERSION >= 140
+# define nssv_RESTORE_WARNINGS() __pragma(warning(pop ))
+#else
+# define nssv_RESTORE_WARNINGS()
+#endif
+
+// Suppress the following MSVC (GSL) warnings:
+// - C4455, non-gsl : 'operator ""sv': literal suffix identifiers that do not
+// start with an underscore are reserved
+// - C26472, gsl::t.1 : don't use a static_cast for arithmetic conversions;
+// use brace initialization, gsl::narrow_cast or gsl::narow
+// - C26481: gsl::b.1 : don't use pointer arithmetic. Use span instead
+
+nssv_DISABLE_MSVC_WARNINGS( 4455 26481 26472 )
+//nssv_DISABLE_CLANG_WARNINGS( "-Wuser-defined-literals" )
+//nssv_DISABLE_GNUC_WARNINGS( -Wliteral-suffix )
+
+namespace nonstd { namespace sv_lite {
+
+namespace detail {
+
+// support constexpr comparison in C++14;
+// for C++17 and later, use provided traits:
+
+template< typename CharT >
+inline nssv_constexpr14 int compare( CharT const * s1, CharT const * s2, std::size_t count )
+{
+ while ( count-- != 0 )
+ {
+ if ( *s1 < *s2 ) return -1;
+ if ( *s1 > *s2 ) return +1;
+ ++s1; ++s2;
+ }
+ return 0;
+}
+
+#if nssv_HAVE_BUILTIN_MEMCMP
+
+// specialization of compare() for char, see also generic compare() above:
+
+inline nssv_constexpr14 int compare( char const * s1, char const * s2, std::size_t count )
+{
+ return nssv_BUILTIN_MEMCMP( s1, s2, count );
+}
+
+#endif
+
+#if nssv_HAVE_BUILTIN_STRLEN
+
+// specialization of length() for char, see also generic length() further below:
+
+inline nssv_constexpr std::size_t length( char const * s )
+{
+ return nssv_BUILTIN_STRLEN( s );
+}
+
+#endif
+
+#if defined(__OPTIMIZE__)
+
+// gcc, clang provide __OPTIMIZE__
+// Expect tail call optimization to make length() non-recursive:
+
+template< typename CharT >
+inline nssv_constexpr std::size_t length( CharT * s, std::size_t result = 0 )
+{
+ return *s == '\0' ? result : length( s + 1, result + 1 );
+}
+
+#else // OPTIMIZE
+
+// non-recursive:
+
+template< typename CharT >
+inline nssv_constexpr14 std::size_t length( CharT * s )
+{
+ std::size_t result = 0;
+ while ( *s++ != '\0' )
+ {
+ ++result;
+ }
+ return result;
+}
+
+#endif // OPTIMIZE
+
+} // namespace detail
+
+template
+<
+ class CharT,
+ class Traits = std::char_traits<CharT>
+>
+class basic_string_view;
+
+//
+// basic_string_view:
+//
+
+template
+<
+ class CharT,
+ class Traits /* = std::char_traits<CharT> */
+>
+class basic_string_view
+{
+public:
+ // Member types:
+
+ typedef Traits traits_type;
+ typedef CharT value_type;
+
+ typedef CharT * pointer;
+ typedef CharT const * const_pointer;
+ typedef CharT & reference;
+ typedef CharT const & const_reference;
+
+ typedef const_pointer iterator;
+ typedef const_pointer const_iterator;
+ typedef std::reverse_iterator< const_iterator > reverse_iterator;
+ typedef std::reverse_iterator< const_iterator > const_reverse_iterator;
+
+ typedef std::size_t size_type;
+ typedef std::ptrdiff_t difference_type;
+
+ // 24.4.2.1 Construction and assignment:
+
+ nssv_constexpr basic_string_view() nssv_noexcept
+ : data_( nssv_nullptr )
+ , size_( 0 )
+ {}
+
+#if nssv_CPP11_OR_GREATER
+ nssv_constexpr basic_string_view( basic_string_view const & other ) nssv_noexcept = default;
+#else
+ nssv_constexpr basic_string_view( basic_string_view const & other ) nssv_noexcept
+ : data_( other.data_)
+ , size_( other.size_)
+ {}
+#endif
+
+ nssv_constexpr basic_string_view( CharT const * s, size_type count ) nssv_noexcept // non-standard noexcept
+ : data_( s )
+ , size_( count )
+ {}
+
+ nssv_constexpr basic_string_view( CharT const * s) nssv_noexcept // non-standard noexcept
+ : data_( s )
+#if nssv_CPP17_OR_GREATER
+ , size_( Traits::length(s) )
+#elif nssv_CPP11_OR_GREATER
+ , size_( detail::length(s) )
+#else
+ , size_( Traits::length(s) )
+#endif
+ {}
+
+ // Assignment:
+
+#if nssv_CPP11_OR_GREATER
+ nssv_constexpr14 basic_string_view & operator=( basic_string_view const & other ) nssv_noexcept = default;
+#else
+ nssv_constexpr14 basic_string_view & operator=( basic_string_view const & other ) nssv_noexcept
+ {
+ data_ = other.data_;
+ size_ = other.size_;
+ return *this;
+ }
+#endif
+
+ // 24.4.2.2 Iterator support:
+
+ nssv_constexpr const_iterator begin() const nssv_noexcept { return data_; }
+ nssv_constexpr const_iterator end() const nssv_noexcept { return data_ + size_; }
+
+ nssv_constexpr const_iterator cbegin() const nssv_noexcept { return begin(); }
+ nssv_constexpr const_iterator cend() const nssv_noexcept { return end(); }
+
+ nssv_constexpr const_reverse_iterator rbegin() const nssv_noexcept { return const_reverse_iterator( end() ); }
+ nssv_constexpr const_reverse_iterator rend() const nssv_noexcept { return const_reverse_iterator( begin() ); }
+
+ nssv_constexpr const_reverse_iterator crbegin() const nssv_noexcept { return rbegin(); }
+ nssv_constexpr const_reverse_iterator crend() const nssv_noexcept { return rend(); }
+
+ // 24.4.2.3 Capacity:
+
+ nssv_constexpr size_type size() const nssv_noexcept { return size_; }
+ nssv_constexpr size_type length() const nssv_noexcept { return size_; }
+ nssv_constexpr size_type max_size() const nssv_noexcept { return (std::numeric_limits< size_type >::max)(); }
+
+ // since C++20
+ nssv_nodiscard nssv_constexpr bool empty() const nssv_noexcept
+ {
+ return 0 == size_;
+ }
+
+ // 24.4.2.4 Element access:
+
+ nssv_constexpr const_reference operator[]( size_type pos ) const
+ {
+ return data_at( pos );
+ }
+
+ nssv_constexpr14 const_reference at( size_type pos ) const
+ {
+#if nssv_CONFIG_NO_EXCEPTIONS
+ assert( pos < size() );
+#else
+ if ( pos >= size() )
+ {
+ throw std::out_of_range("nonstd::string_view::at()");
+ }
+#endif
+ return data_at( pos );
+ }
+
+ nssv_constexpr const_reference front() const { return data_at( 0 ); }
+ nssv_constexpr const_reference back() const { return data_at( size() - 1 ); }
+
+ nssv_constexpr const_pointer data() const nssv_noexcept { return data_; }
+
+ // 24.4.2.5 Modifiers:
+
+ nssv_constexpr14 void remove_prefix( size_type n )
+ {
+ assert( n <= size() );
+ data_ += n;
+ size_ -= n;
+ }
+
+ nssv_constexpr14 void remove_suffix( size_type n )
+ {
+ assert( n <= size() );
+ size_ -= n;
+ }
+
+ nssv_constexpr14 void swap( basic_string_view & other ) nssv_noexcept
+ {
+ const basic_string_view tmp(other);
+ other = *this;
+ *this = tmp;
+ }
+
+ // 24.4.2.6 String operations:
+
+ size_type copy( CharT * dest, size_type n, size_type pos = 0 ) const
+ {
+#if nssv_CONFIG_NO_EXCEPTIONS
+ assert( pos <= size() );
+#else
+ if ( pos > size() )
+ {
+ throw std::out_of_range("nonstd::string_view::copy()");
+ }
+#endif
+ const size_type rlen = (std::min)( n, size() - pos );
+
+ (void) Traits::copy( dest, data() + pos, rlen );
+
+ return rlen;
+ }
+
+ nssv_constexpr14 basic_string_view substr( size_type pos = 0, size_type n = npos ) const
+ {
+#if nssv_CONFIG_NO_EXCEPTIONS
+ assert( pos <= size() );
+#else
+ if ( pos > size() )
+ {
+ throw std::out_of_range("nonstd::string_view::substr()");
+ }
+#endif
+ return basic_string_view( data() + pos, (std::min)( n, size() - pos ) );
+ }
+
+ // compare(), 6x:
+
+ nssv_constexpr14 int compare( basic_string_view other ) const nssv_noexcept // (1)
+ {
+#if nssv_CPP17_OR_GREATER
+ if ( const int result = Traits::compare( data(), other.data(), (std::min)( size(), other.size() ) ) )
+#else
+ if ( const int result = detail::compare( data(), other.data(), (std::min)( size(), other.size() ) ) )
+#endif
+ {
+ return result;
+ }
+
+ return size() == other.size() ? 0 : size() < other.size() ? -1 : 1;
+ }
+
+ nssv_constexpr int compare( size_type pos1, size_type n1, basic_string_view other ) const // (2)
+ {
+ return substr( pos1, n1 ).compare( other );
+ }
+
+ nssv_constexpr int compare( size_type pos1, size_type n1, basic_string_view other, size_type pos2, size_type n2 ) const // (3)
+ {
+ return substr( pos1, n1 ).compare( other.substr( pos2, n2 ) );
+ }
+
+ nssv_constexpr int compare( CharT const * s ) const // (4)
+ {
+ return compare( basic_string_view( s ) );
+ }
+
+ nssv_constexpr int compare( size_type pos1, size_type n1, CharT const * s ) const // (5)
+ {
+ return substr( pos1, n1 ).compare( basic_string_view( s ) );
+ }
+
+ nssv_constexpr int compare( size_type pos1, size_type n1, CharT const * s, size_type n2 ) const // (6)
+ {
+ return substr( pos1, n1 ).compare( basic_string_view( s, n2 ) );
+ }
+
+ // 24.4.2.7 Searching:
+
+ // starts_with(), 3x, since C++20:
+
+ nssv_constexpr bool starts_with( basic_string_view v ) const nssv_noexcept // (1)
+ {
+ return size() >= v.size() && compare( 0, v.size(), v ) == 0;
+ }
+
+ nssv_constexpr bool starts_with( CharT c ) const nssv_noexcept // (2)
+ {
+ return starts_with( basic_string_view( &c, 1 ) );
+ }
+
+ nssv_constexpr bool starts_with( CharT const * s ) const // (3)
+ {
+ return starts_with( basic_string_view( s ) );
+ }
+
+ // ends_with(), 3x, since C++20:
+
+ nssv_constexpr bool ends_with( basic_string_view v ) const nssv_noexcept // (1)
+ {
+ return size() >= v.size() && compare( size() - v.size(), npos, v ) == 0;
+ }
+
+ nssv_constexpr bool ends_with( CharT c ) const nssv_noexcept // (2)
+ {
+ return ends_with( basic_string_view( &c, 1 ) );
+ }
+
+ nssv_constexpr bool ends_with( CharT const * s ) const // (3)
+ {
+ return ends_with( basic_string_view( s ) );
+ }
+
+ // find(), 4x:
+
+ nssv_constexpr14 size_type find( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1)
+ {
+ return assert( v.size() == 0 || v.data() != nssv_nullptr )
+ , pos >= size()
+ ? npos
+ : to_pos( std::search( cbegin() + pos, cend(), v.cbegin(), v.cend(), Traits::eq ) );
+ }
+
+ nssv_constexpr14 size_type find( CharT c, size_type pos = 0 ) const nssv_noexcept // (2)
+ {
+ return find( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr14 size_type find( CharT const * s, size_type pos, size_type n ) const // (3)
+ {
+ return find( basic_string_view( s, n ), pos );
+ }
+
+ nssv_constexpr14 size_type find( CharT const * s, size_type pos = 0 ) const // (4)
+ {
+ return find( basic_string_view( s ), pos );
+ }
+
+ // rfind(), 4x:
+
+ nssv_constexpr14 size_type rfind( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1)
+ {
+ if ( size() < v.size() )
+ {
+ return npos;
+ }
+
+ if ( v.empty() )
+ {
+ return (std::min)( size(), pos );
+ }
+
+ const_iterator last = cbegin() + (std::min)( size() - v.size(), pos ) + v.size();
+ const_iterator result = std::find_end( cbegin(), last, v.cbegin(), v.cend(), Traits::eq );
+
+ return result != last ? size_type( result - cbegin() ) : npos;
+ }
+
+ nssv_constexpr14 size_type rfind( CharT c, size_type pos = npos ) const nssv_noexcept // (2)
+ {
+ return rfind( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr14 size_type rfind( CharT const * s, size_type pos, size_type n ) const // (3)
+ {
+ return rfind( basic_string_view( s, n ), pos );
+ }
+
+ nssv_constexpr14 size_type rfind( CharT const * s, size_type pos = npos ) const // (4)
+ {
+ return rfind( basic_string_view( s ), pos );
+ }
+
+ // find_first_of(), 4x:
+
+ nssv_constexpr size_type find_first_of( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1)
+ {
+ return pos >= size()
+ ? npos
+ : to_pos( std::find_first_of( cbegin() + pos, cend(), v.cbegin(), v.cend(), Traits::eq ) );
+ }
+
+ nssv_constexpr size_type find_first_of( CharT c, size_type pos = 0 ) const nssv_noexcept // (2)
+ {
+ return find_first_of( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr size_type find_first_of( CharT const * s, size_type pos, size_type n ) const // (3)
+ {
+ return find_first_of( basic_string_view( s, n ), pos );
+ }
+
+ nssv_constexpr size_type find_first_of( CharT const * s, size_type pos = 0 ) const // (4)
+ {
+ return find_first_of( basic_string_view( s ), pos );
+ }
+
+ // find_last_of(), 4x:
+
+ nssv_constexpr size_type find_last_of( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1)
+ {
+ return empty()
+ ? npos
+ : pos >= size()
+ ? find_last_of( v, size() - 1 )
+ : to_pos( std::find_first_of( const_reverse_iterator( cbegin() + pos + 1 ), crend(), v.cbegin(), v.cend(), Traits::eq ) );
+ }
+
+ nssv_constexpr size_type find_last_of( CharT c, size_type pos = npos ) const nssv_noexcept // (2)
+ {
+ return find_last_of( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr size_type find_last_of( CharT const * s, size_type pos, size_type count ) const // (3)
+ {
+ return find_last_of( basic_string_view( s, count ), pos );
+ }
+
+ nssv_constexpr size_type find_last_of( CharT const * s, size_type pos = npos ) const // (4)
+ {
+ return find_last_of( basic_string_view( s ), pos );
+ }
+
+ // find_first_not_of(), 4x:
+
+ nssv_constexpr size_type find_first_not_of( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1)
+ {
+ return pos >= size()
+ ? npos
+ : to_pos( std::find_if( cbegin() + pos, cend(), not_in_view( v ) ) );
+ }
+
+ nssv_constexpr size_type find_first_not_of( CharT c, size_type pos = 0 ) const nssv_noexcept // (2)
+ {
+ return find_first_not_of( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr size_type find_first_not_of( CharT const * s, size_type pos, size_type count ) const // (3)
+ {
+ return find_first_not_of( basic_string_view( s, count ), pos );
+ }
+
+ nssv_constexpr size_type find_first_not_of( CharT const * s, size_type pos = 0 ) const // (4)
+ {
+ return find_first_not_of( basic_string_view( s ), pos );
+ }
+
+ // find_last_not_of(), 4x:
+
+ nssv_constexpr size_type find_last_not_of( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1)
+ {
+ return empty()
+ ? npos
+ : pos >= size()
+ ? find_last_not_of( v, size() - 1 )
+ : to_pos( std::find_if( const_reverse_iterator( cbegin() + pos + 1 ), crend(), not_in_view( v ) ) );
+ }
+
+ nssv_constexpr size_type find_last_not_of( CharT c, size_type pos = npos ) const nssv_noexcept // (2)
+ {
+ return find_last_not_of( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr size_type find_last_not_of( CharT const * s, size_type pos, size_type count ) const // (3)
+ {
+ return find_last_not_of( basic_string_view( s, count ), pos );
+ }
+
+ nssv_constexpr size_type find_last_not_of( CharT const * s, size_type pos = npos ) const // (4)
+ {
+ return find_last_not_of( basic_string_view( s ), pos );
+ }
+
+ // Constants:
+
+#if nssv_CPP17_OR_GREATER
+ static nssv_constexpr size_type npos = size_type(-1);
+#elif nssv_CPP11_OR_GREATER
+ enum : size_type { npos = size_type(-1) };
+#else
+ enum { npos = size_type(-1) };
+#endif
+
+private:
+ struct not_in_view
+ {
+ const basic_string_view v;
+
+ nssv_constexpr explicit not_in_view( basic_string_view v_ ) : v( v_ ) {}
+
+ nssv_constexpr bool operator()( CharT c ) const
+ {
+ return npos == v.find_first_of( c );
+ }
+ };
+
+ nssv_constexpr size_type to_pos( const_iterator it ) const
+ {
+ return it == cend() ? npos : size_type( it - cbegin() );
+ }
+
+ nssv_constexpr size_type to_pos( const_reverse_iterator it ) const
+ {
+ return it == crend() ? npos : size_type( crend() - it - 1 );
+ }
+
+ nssv_constexpr const_reference data_at( size_type pos ) const
+ {
+#if nssv_BETWEEN( nssv_COMPILER_GNUC_VERSION, 1, 500 )
+ return data_[pos];
+#else
+ return assert( pos < size() ), data_[pos];
+#endif
+ }
+
+private:
+ const_pointer data_;
+ size_type size_;
+
+public:
+#if nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+
+ template< class Allocator >
+ basic_string_view( std::basic_string<CharT, Traits, Allocator> const & s ) nssv_noexcept
+ : data_( s.data() )
+ , size_( s.size() )
+ {}
+
+#if nssv_HAVE_EXPLICIT_CONVERSION
+
+ template< class Allocator >
+ explicit operator std::basic_string<CharT, Traits, Allocator>() const
+ {
+ return to_string( Allocator() );
+ }
+
+#endif // nssv_HAVE_EXPLICIT_CONVERSION
+
+#if nssv_CPP11_OR_GREATER
+
+ template< class Allocator = std::allocator<CharT> >
+ std::basic_string<CharT, Traits, Allocator>
+ to_string( Allocator const & a = Allocator() ) const
+ {
+ return std::basic_string<CharT, Traits, Allocator>( begin(), end(), a );
+ }
+
+#else
+
+ std::basic_string<CharT, Traits>
+ to_string() const
+ {
+ return std::basic_string<CharT, Traits>( begin(), end() );
+ }
+
+ template< class Allocator >
+ std::basic_string<CharT, Traits, Allocator>
+ to_string( Allocator const & a ) const
+ {
+ return std::basic_string<CharT, Traits, Allocator>( begin(), end(), a );
+ }
+
+#endif // nssv_CPP11_OR_GREATER
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+};
+
+//
+// Non-member functions:
+//
+
+// 24.4.3 Non-member comparison functions:
+// lexicographically compare two string views (function template):
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator== (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator!= (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator< (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator<= (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator> (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator>= (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+// Let S be basic_string_view<CharT, Traits>, and sv be an instance of S.
+// Implementations shall provide sufficient additional overloads marked
+// constexpr and noexcept so that an object t with an implicit conversion
+// to S can be compared according to Table 67.
+
+#if ! nssv_CPP11_OR_GREATER || nssv_BETWEEN( nssv_COMPILER_MSVC_VERSION, 100, 141 )
+
+// accommodate for older compilers:
+
+// ==
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.size() == detail::length( rhs ) && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return detail::length( lhs ) == rhs.size() && rhs.compare( lhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+// !=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+// <
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) > 0; }
+
+// <=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) >= 0; }
+
+// >
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) < 0; }
+
+// >=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) <= 0; }
+
+#else // newer compilers:
+
+#define nssv_BASIC_STRING_VIEW_I(T,U) typename std::decay< basic_string_view<T,U> >::type
+
+#if defined(_MSC_VER) // issue 40
+# define nssv_MSVC_ORDER(x) , int=x
+#else
+# define nssv_MSVC_ORDER(x) /*, int=x*/
+#endif
+
+// ==
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator==(
+ basic_string_view <CharT, Traits> lhs,
+ nssv_BASIC_STRING_VIEW_I(CharT, Traits) rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator==(
+ nssv_BASIC_STRING_VIEW_I(CharT, Traits) lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+// !=
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator!= (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator!= (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+// <
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator< (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator< (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+// <=
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator<= (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator<= (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+// >
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator> (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator> (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+// >=
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator>= (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator>= (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+#undef nssv_MSVC_ORDER
+#undef nssv_BASIC_STRING_VIEW_I
+
+#endif // compiler-dependent approach to comparisons
+
+// 24.4.4 Inserters and extractors:
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+
+namespace detail {
+
+template< class Stream >
+void write_padding( Stream & os, std::streamsize n )
+{
+ for ( std::streamsize i = 0; i < n; ++i )
+ os.rdbuf()->sputc( os.fill() );
+}
+
+template< class Stream, class View >
+Stream & write_to_stream( Stream & os, View const & sv )
+{
+ typename Stream::sentry sentry( os );
+
+ if ( !os )
+ return os;
+
+ const std::streamsize length = static_cast<std::streamsize>( sv.length() );
+
+ // Whether, and how, to pad:
+ const bool pad = ( length < os.width() );
+ const bool left_pad = pad && ( os.flags() & std::ios_base::adjustfield ) == std::ios_base::right;
+
+ if ( left_pad )
+ write_padding( os, os.width() - length );
+
+ // Write span characters:
+ os.rdbuf()->sputn( sv.begin(), length );
+
+ if ( pad && !left_pad )
+ write_padding( os, os.width() - length );
+
+ // Reset output stream width:
+ os.width( 0 );
+
+ return os;
+}
+
+} // namespace detail
+
+template< class CharT, class Traits >
+std::basic_ostream<CharT, Traits> &
+operator<<(
+ std::basic_ostream<CharT, Traits>& os,
+ basic_string_view <CharT, Traits> sv )
+{
+ return detail::write_to_stream( os, sv );
+}
+
+#endif // nssv_CONFIG_NO_STREAM_INSERTION
+
+// Several typedefs for common character types are provided:
+
+typedef basic_string_view<char> string_view;
+typedef basic_string_view<wchar_t> wstring_view;
+#if nssv_HAVE_WCHAR16_T
+typedef basic_string_view<char16_t> u16string_view;
+typedef basic_string_view<char32_t> u32string_view;
+#endif
+
+}} // namespace nonstd::sv_lite
+
+//
+// 24.4.6 Suffix for basic_string_view literals:
+//
+
+#if nssv_HAVE_USER_DEFINED_LITERALS
+
+namespace nonstd {
+nssv_inline_ns namespace literals {
+nssv_inline_ns namespace string_view_literals {
+
+#if nssv_CONFIG_STD_SV_OPERATOR && nssv_HAVE_STD_DEFINED_LITERALS
+
+nssv_constexpr nonstd::sv_lite::string_view operator "" sv( const char* str, size_t len ) nssv_noexcept // (1)
+{
+ return nonstd::sv_lite::string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u16string_view operator "" sv( const char16_t* str, size_t len ) nssv_noexcept // (2)
+{
+ return nonstd::sv_lite::u16string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u32string_view operator "" sv( const char32_t* str, size_t len ) nssv_noexcept // (3)
+{
+ return nonstd::sv_lite::u32string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::wstring_view operator "" sv( const wchar_t* str, size_t len ) nssv_noexcept // (4)
+{
+ return nonstd::sv_lite::wstring_view{ str, len };
+}
+
+#endif // nssv_CONFIG_STD_SV_OPERATOR && nssv_HAVE_STD_DEFINED_LITERALS
+
+#if nssv_CONFIG_USR_SV_OPERATOR
+
+nssv_constexpr nonstd::sv_lite::string_view operator "" _sv( const char* str, size_t len ) nssv_noexcept // (1)
+{
+ return nonstd::sv_lite::string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u16string_view operator "" _sv( const char16_t* str, size_t len ) nssv_noexcept // (2)
+{
+ return nonstd::sv_lite::u16string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u32string_view operator "" _sv( const char32_t* str, size_t len ) nssv_noexcept // (3)
+{
+ return nonstd::sv_lite::u32string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::wstring_view operator "" _sv( const wchar_t* str, size_t len ) nssv_noexcept // (4)
+{
+ return nonstd::sv_lite::wstring_view{ str, len };
+}
+
+#endif // nssv_CONFIG_USR_SV_OPERATOR
+
+}}} // namespace nonstd::literals::string_view_literals
+
+#endif
+
+//
+// Extensions for std::string:
+//
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+namespace sv_lite {
+
+// Exclude MSVC 14 (19.00): it yields ambiguous to_string():
+
+#if nssv_CPP11_OR_GREATER && nssv_COMPILER_MSVC_VERSION != 140
+
+template< class CharT, class Traits, class Allocator = std::allocator<CharT> >
+std::basic_string<CharT, Traits, Allocator>
+to_string( basic_string_view<CharT, Traits> v, Allocator const & a = Allocator() )
+{
+ return std::basic_string<CharT,Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+#else
+
+template< class CharT, class Traits >
+std::basic_string<CharT, Traits>
+to_string( basic_string_view<CharT, Traits> v )
+{
+ return std::basic_string<CharT, Traits>( v.begin(), v.end() );
+}
+
+template< class CharT, class Traits, class Allocator >
+std::basic_string<CharT, Traits, Allocator>
+to_string( basic_string_view<CharT, Traits> v, Allocator const & a )
+{
+ return std::basic_string<CharT, Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+#endif // nssv_CPP11_OR_GREATER
+
+template< class CharT, class Traits, class Allocator >
+basic_string_view<CharT, Traits>
+to_string_view( std::basic_string<CharT, Traits, Allocator> const & s )
+{
+ return basic_string_view<CharT, Traits>( s.data(), s.size() );
+}
+
+}} // namespace nonstd::sv_lite
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+//
+// make types and algorithms available in namespace nonstd:
+//
+
+namespace nonstd {
+
+using sv_lite::basic_string_view;
+using sv_lite::string_view;
+using sv_lite::wstring_view;
+
+#if nssv_HAVE_WCHAR16_T
+using sv_lite::u16string_view;
+#endif
+#if nssv_HAVE_WCHAR32_T
+using sv_lite::u32string_view;
+#endif
+
+// literal "sv"
+
+using sv_lite::operator==;
+using sv_lite::operator!=;
+using sv_lite::operator<;
+using sv_lite::operator<=;
+using sv_lite::operator>;
+using sv_lite::operator>=;
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+using sv_lite::operator<<;
+#endif
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+using sv_lite::to_string;
+using sv_lite::to_string_view;
+#endif
+
+} // namespace nonstd
+
+// 24.4.5 Hash support (C++11):
+
+// Note: The hash value of a string view object is equal to the hash value of
+// the corresponding string object.
+
+#if nssv_HAVE_STD_HASH
+
+#include <functional>
+
+namespace std {
+
+template<>
+struct hash< nonstd::string_view >
+{
+public:
+ std::size_t operator()( nonstd::string_view v ) const nssv_noexcept
+ {
+ return std::hash<std::string>()( std::string( v.data(), v.size() ) );
+ }
+};
+
+template<>
+struct hash< nonstd::wstring_view >
+{
+public:
+ std::size_t operator()( nonstd::wstring_view v ) const nssv_noexcept
+ {
+ return std::hash<std::wstring>()( std::wstring( v.data(), v.size() ) );
+ }
+};
+
+template<>
+struct hash< nonstd::u16string_view >
+{
+public:
+ std::size_t operator()( nonstd::u16string_view v ) const nssv_noexcept
+ {
+ return std::hash<std::u16string>()( std::u16string( v.data(), v.size() ) );
+ }
+};
+
+template<>
+struct hash< nonstd::u32string_view >
+{
+public:
+ std::size_t operator()( nonstd::u32string_view v ) const nssv_noexcept
+ {
+ return std::hash<std::u32string>()( std::u32string( v.data(), v.size() ) );
+ }
+};
+
+} // namespace std
+
+#endif // nssv_HAVE_STD_HASH
+
+nssv_RESTORE_WARNINGS()
+
+#endif // nssv_HAVE_STD_STRING_VIEW
+#endif // NONSTD_SV_LITE_H_INCLUDED
+//!
+//! termcolor
+//! ~~~~~~~~~
+//!
+//! termcolor is a header-only c++ library for printing colored messages
+//! to the terminal. Written just for fun with a help of the Force.
+//!
+//! :copyright: (c) 2013 by Ihor Kalnytskyi
+//! :license: BSD, see LICENSE for details
+//!
+
+#ifndef TERMCOLOR_HPP_
+#define TERMCOLOR_HPP_
+
+// the following snippet of code detects the current OS and
+// defines the appropriate macro that is used to wrap some
+// platform specific things
+#if defined(_WIN32) || defined(_WIN64)
+#define TERMCOLOR_OS_WINDOWS
+#elif defined(__APPLE__)
+#define TERMCOLOR_OS_MACOS
+#elif defined(__unix__) || defined(__unix)
+#define TERMCOLOR_OS_LINUX
+#else
+#error unsupported platform
+#endif
+
+// This headers provides the `isatty()`/`fileno()` functions,
+// which are used for testing whether a standart stream refers
+// to the terminal. As for Windows, we also need WinApi funcs
+// for changing colors attributes of the terminal.
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+#include <unistd.h>
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#include <io.h>
+#include <windows.h>
+#endif
+
+#include <cstdio>
+#include <iostream>
+
+namespace termcolor {
+// Forward declaration of the `_internal` namespace.
+// All comments are below.
+namespace _internal {
+// An index to be used to access a private storage of I/O streams. See
+// colorize / nocolorize I/O manipulators for details.
+static int colorize_index = std::ios_base::xalloc();
+
+inline FILE *get_standard_stream(const std::ostream &stream);
+inline bool is_colorized(std::ostream &stream);
+inline bool is_atty(const std::ostream &stream);
+
+#if defined(TERMCOLOR_OS_WINDOWS)
+inline void win_change_attributes(std::ostream &stream, int foreground, int background = -1);
+#endif
+} // namespace _internal
+
+inline std::ostream &colorize(std::ostream &stream) {
+ stream.iword(_internal::colorize_index) = 1L;
+ return stream;
+}
+
+inline std::ostream &nocolorize(std::ostream &stream) {
+ stream.iword(_internal::colorize_index) = 0L;
+ return stream;
+}
+
+inline std::ostream &reset(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[00m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, -1);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &bold(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[1m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &dark(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[2m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &italic(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[3m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &underline(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[4m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &blink(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[5m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &reverse(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[7m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &concealed(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[8m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &crossed(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[9m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &grey(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[30m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream,
+ 0 // grey (black)
+ );
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &red(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[31m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &green(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[32m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_GREEN);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &yellow(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[33m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_GREEN | FOREGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &blue(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[34m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_BLUE);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &magenta(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[35m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &cyan(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[36m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_GREEN);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &white(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[37m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_grey(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[40m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1,
+ 0 // grey (black)
+ );
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_red(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[41m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_green(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[42m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_yellow(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[43m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN | BACKGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_blue(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[44m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_BLUE);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_magenta(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[45m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_BLUE | BACKGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_cyan(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[46m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN | BACKGROUND_BLUE);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_white(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[47m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1,
+ BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_RED);
+#endif
+ }
+
+ return stream;
+}
+
+//! Since C++ hasn't a way to hide something in the header from
+//! the outer access, I have to introduce this namespace which
+//! is used for internal purpose and should't be access from
+//! the user code.
+namespace _internal {
+//! Since C++ hasn't a true way to extract stream handler
+//! from the a given `std::ostream` object, I have to write
+//! this kind of hack.
+inline FILE *get_standard_stream(const std::ostream &stream) {
+ if (&stream == &std::cout)
+ return stdout;
+ else if ((&stream == &std::cerr) || (&stream == &std::clog))
+ return stderr;
+
+ return 0;
+}
+
+// Say whether a given stream should be colorized or not. It's always
+// true for ATTY streams and may be true for streams marked with
+// colorize flag.
+inline bool is_colorized(std::ostream &stream) {
+ return is_atty(stream) || static_cast<bool>(stream.iword(colorize_index));
+}
+
+//! Test whether a given `std::ostream` object refers to
+//! a terminal.
+inline bool is_atty(const std::ostream &stream) {
+ FILE *std_stream = get_standard_stream(stream);
+
+ // Unfortunately, fileno() ends with segmentation fault
+ // if invalid file descriptor is passed. So we need to
+ // handle this case gracefully and assume it's not a tty
+ // if standard stream is not detected, and 0 is returned.
+ if (!std_stream)
+ return false;
+
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ return ::isatty(fileno(std_stream));
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ return ::_isatty(_fileno(std_stream));
+#endif
+}
+
+#if defined(TERMCOLOR_OS_WINDOWS)
+//! Change Windows Terminal colors attribute. If some
+//! parameter is `-1` then attribute won't changed.
+inline void win_change_attributes(std::ostream &stream, int foreground, int background) {
+ // yeah, i know.. it's ugly, it's windows.
+ static WORD defaultAttributes = 0;
+
+ // Windows doesn't have ANSI escape sequences and so we use special
+ // API to change Terminal output color. That means we can't
+ // manipulate colors by means of "std::stringstream" and hence
+ // should do nothing in this case.
+ if (!_internal::is_atty(stream))
+ return;
+
+ // get terminal handle
+ HANDLE hTerminal = INVALID_HANDLE_VALUE;
+ if (&stream == &std::cout)
+ hTerminal = GetStdHandle(STD_OUTPUT_HANDLE);
+ else if (&stream == &std::cerr)
+ hTerminal = GetStdHandle(STD_ERROR_HANDLE);
+
+ // save default terminal attributes if it unsaved
+ if (!defaultAttributes) {
+ CONSOLE_SCREEN_BUFFER_INFO info;
+ if (!GetConsoleScreenBufferInfo(hTerminal, &info))
+ return;
+ defaultAttributes = info.wAttributes;
+ }
+
+ // restore all default settings
+ if (foreground == -1 && background == -1) {
+ SetConsoleTextAttribute(hTerminal, defaultAttributes);
+ return;
+ }
+
+ // get current settings
+ CONSOLE_SCREEN_BUFFER_INFO info;
+ if (!GetConsoleScreenBufferInfo(hTerminal, &info))
+ return;
+
+ if (foreground != -1) {
+ info.wAttributes &= ~(info.wAttributes & 0x0F);
+ info.wAttributes |= static_cast<WORD>(foreground);
+ }
+
+ if (background != -1) {
+ info.wAttributes &= ~(info.wAttributes & 0xF0);
+ info.wAttributes |= static_cast<WORD>(background);
+ }
+
+ SetConsoleTextAttribute(hTerminal, info.wAttributes);
+}
+#endif // TERMCOLOR_OS_WINDOWS
+
+} // namespace _internal
+
+} // namespace termcolor
+
+#undef TERMCOLOR_OS_WINDOWS
+#undef TERMCOLOR_OS_MACOS
+#undef TERMCOLOR_OS_LINUX
+
+#endif // TERMCOLOR_HPP_
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <cstdint>
+#include <string>
+
+#include <clocale>
+#include <locale>
+
+#include <cstdlib>
+// #include <tabulate/termcolor.hpp>
+#include <wchar.h>
+
+namespace tabulate {
+
+#if defined(__unix__) || defined(__unix) || defined(__APPLE__)
+inline int get_wcswidth(const std::string &string, const std::string &locale,
+ size_t max_column_width) {
+ if (string.size() == 0)
+ return 0;
+
+ // The behavior of wcswidth() depends on the LC_CTYPE category of the current locale.
+ // Set the current locale based on cell properties before computing width
+ auto old_locale = std::locale::global(std::locale(locale));
+
+ // Convert from narrow std::string to wide string
+ wchar_t *wide_string = new wchar_t[string.size()];
+ std::mbstowcs(wide_string, string.c_str(), string.size());
+
+ // Compute display width of wide string
+ int result = wcswidth(wide_string, max_column_width);
+ delete[] wide_string;
+
+ // Restore old locale
+ std::locale::global(old_locale);
+
+ return result;
+}
+#endif
+
+inline size_t get_sequence_length(const std::string &text, const std::string &locale,
+ bool is_multi_byte_character_support_enabled) {
+ if (!is_multi_byte_character_support_enabled)
+ return text.length();
+
+#if defined(_WIN32) || defined(_WIN64)
+ return (text.length() - std::count_if(text.begin(), text.end(),
+ [](char c) -> bool { return (c & 0xC0) == 0x80; }));
+#elif defined(__unix__) || defined(__unix) || defined(__APPLE__)
+ auto result = get_wcswidth(text, locale, text.size());
+ if (result >= 0)
+ return result;
+ else
+ return (text.length() - std::count_if(text.begin(), text.end(),
+ [](char c) -> bool { return (c & 0xC0) == 0x80; }));
+#endif
+}
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/termcolor.hpp>
+
+namespace tabulate {
+
+enum class Color { none, grey, red, green, yellow, blue, magenta, cyan, white };
+}
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+enum class FontAlign { left, right, center };
+}
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+enum class FontStyle { bold, dark, italic, underline, blink, reverse, concealed, crossed };
+}
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <iostream>
+#include <memory>
+#include <string>
+// #include <tabulate/format.hpp>
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#include <algorithm>
+#include <cctype>
+#include <cstddef>
+#include <sstream>
+#include <string>
+// #include <tabulate/color.hpp>
+// #include <tabulate/font_align.hpp>
+// #include <tabulate/font_style.hpp>
+// #include <tabulate/utf8.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+
+namespace tabulate {
+
+class Format {
+public:
+ Format &width(size_t value) {
+ width_ = value;
+ return *this;
+ }
+
+ Format &height(size_t value) {
+ height_ = value;
+ return *this;
+ }
+
+ Format &padding(size_t value) {
+ padding_left_ = value;
+ padding_right_ = value;
+ padding_top_ = value;
+ padding_bottom_ = value;
+ return *this;
+ }
+
+ Format &padding_left(size_t value) {
+ padding_left_ = value;
+ return *this;
+ }
+
+ Format &padding_right(size_t value) {
+ padding_right_ = value;
+ return *this;
+ }
+
+ Format &padding_top(size_t value) {
+ padding_top_ = value;
+ return *this;
+ }
+
+ Format &padding_bottom(size_t value) {
+ padding_bottom_ = value;
+ return *this;
+ }
+
+ Format &border(const std::string &value) {
+ border_left_ = value;
+ border_right_ = value;
+ border_top_ = value;
+ border_bottom_ = value;
+ return *this;
+ }
+
+ Format &border_color(Color value) {
+ border_left_color_ = value;
+ border_right_color_ = value;
+ border_top_color_ = value;
+ border_bottom_color_ = value;
+ return *this;
+ }
+
+ Format &border_background_color(Color value) {
+ border_left_background_color_ = value;
+ border_right_background_color_ = value;
+ border_top_background_color_ = value;
+ border_bottom_background_color_ = value;
+ return *this;
+ }
+
+ Format &border_left(const std::string &value) {
+ border_left_ = value;
+ return *this;
+ }
+
+ Format &border_left_color(Color value) {
+ border_left_color_ = value;
+ return *this;
+ }
+
+ Format &border_left_background_color(Color value) {
+ border_left_background_color_ = value;
+ return *this;
+ }
+
+ Format &border_right(const std::string &value) {
+ border_right_ = value;
+ return *this;
+ }
+
+ Format &border_right_color(Color value) {
+ border_right_color_ = value;
+ return *this;
+ }
+
+ Format &border_right_background_color(Color value) {
+ border_right_background_color_ = value;
+ return *this;
+ }
+
+ Format &border_top(const std::string &value) {
+ border_top_ = value;
+ return *this;
+ }
+
+ Format &border_top_color(Color value) {
+ border_top_color_ = value;
+ return *this;
+ }
+
+ Format &border_top_background_color(Color value) {
+ border_top_background_color_ = value;
+ return *this;
+ }
+
+ Format &border_bottom(const std::string &value) {
+ border_bottom_ = value;
+ return *this;
+ }
+
+ Format &border_bottom_color(Color value) {
+ border_bottom_color_ = value;
+ return *this;
+ }
+
+ Format &border_bottom_background_color(Color value) {
+ border_bottom_background_color_ = value;
+ return *this;
+ }
+
+ Format &show_border() {
+ show_border_top_ = true;
+ show_border_bottom_ = true;
+ show_border_left_ = true;
+ show_border_right_ = true;
+ return *this;
+ }
+
+ Format &hide_border() {
+ show_border_top_ = false;
+ show_border_bottom_ = false;
+ show_border_left_ = false;
+ show_border_right_ = false;
+ return *this;
+ }
+
+ Format &show_border_top() {
+ show_border_top_ = true;
+ return *this;
+ }
+
+ Format &hide_border_top() {
+ show_border_top_ = false;
+ return *this;
+ }
+
+ Format &show_border_bottom() {
+ show_border_bottom_ = true;
+ return *this;
+ }
+
+ Format &hide_border_bottom() {
+ show_border_bottom_ = false;
+ return *this;
+ }
+
+ Format &show_border_left() {
+ show_border_left_ = true;
+ return *this;
+ }
+
+ Format &hide_border_left() {
+ show_border_left_ = false;
+ return *this;
+ }
+
+ Format &show_border_right() {
+ show_border_right_ = true;
+ return *this;
+ }
+
+ Format &hide_border_right() {
+ show_border_right_ = false;
+ return *this;
+ }
+
+ Format &corner(const std::string &value) {
+ corner_top_left_ = value;
+ corner_top_right_ = value;
+ corner_bottom_left_ = value;
+ corner_bottom_right_ = value;
+ return *this;
+ }
+
+ Format &corner_color(Color value) {
+ corner_top_left_color_ = value;
+ corner_top_right_color_ = value;
+ corner_bottom_left_color_ = value;
+ corner_bottom_right_color_ = value;
+ return *this;
+ }
+
+ Format &corner_background_color(Color value) {
+ corner_top_left_background_color_ = value;
+ corner_top_right_background_color_ = value;
+ corner_bottom_left_background_color_ = value;
+ corner_bottom_right_background_color_ = value;
+ return *this;
+ }
+
+ Format &corner_top_left(const std::string &value) {
+ corner_top_left_ = value;
+ return *this;
+ }
+
+ Format &corner_top_left_color(Color value) {
+ corner_top_left_color_ = value;
+ return *this;
+ }
+
+ Format &corner_top_left_background_color(Color value) {
+ corner_top_left_background_color_ = value;
+ return *this;
+ }
+
+ Format &corner_top_right(const std::string &value) {
+ corner_top_right_ = value;
+ return *this;
+ }
+
+ Format &corner_top_right_color(Color value) {
+ corner_top_right_color_ = value;
+ return *this;
+ }
+
+ Format &corner_top_right_background_color(Color value) {
+ corner_top_right_background_color_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_left(const std::string &value) {
+ corner_bottom_left_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_left_color(Color value) {
+ corner_bottom_left_color_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_left_background_color(Color value) {
+ corner_bottom_left_background_color_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_right(const std::string &value) {
+ corner_bottom_right_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_right_color(Color value) {
+ corner_bottom_right_color_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_right_background_color(Color value) {
+ corner_bottom_right_background_color_ = value;
+ return *this;
+ }
+
+ Format &column_separator(const std::string &value) {
+ column_separator_ = value;
+ return *this;
+ }
+
+ Format &column_separator_color(Color value) {
+ column_separator_color_ = value;
+ return *this;
+ }
+
+ Format &column_separator_background_color(Color value) {
+ column_separator_background_color_ = value;
+ return *this;
+ }
+
+ Format &font_align(FontAlign value) {
+ font_align_ = value;
+ return *this;
+ }
+
+ Format &font_style(const std::vector<FontStyle> &style) {
+ if (font_style_.has_value()) {
+ for (auto &s : style)
+ font_style_->push_back(s);
+ } else {
+ font_style_ = style;
+ }
+ return *this;
+ }
+
+ Format &font_color(Color value) {
+ font_color_ = value;
+ return *this;
+ }
+
+ Format &font_background_color(Color value) {
+ font_background_color_ = value;
+ return *this;
+ }
+
+ Format &color(Color value) {
+ font_color(value);
+ border_color(value);
+ corner_color(value);
+ return *this;
+ }
+
+ Format &background_color(Color value) {
+ font_background_color(value);
+ border_background_color(value);
+ corner_background_color(value);
+ return *this;
+ }
+
+ Format &multi_byte_characters(bool value) {
+ multi_byte_characters_ = value;
+ return *this;
+ }
+
+ Format &locale(const std::string &value) {
+ locale_ = value;
+ return *this;
+ }
+
+ // Apply word wrap
+ // Given an input string and a line length, this will insert \n
+ // in strategic places in input string and apply word wrapping
+ static std::string word_wrap(const std::string &str, size_t width, const std::string &locale,
+ bool is_multi_byte_character_support_enabled) {
+ std::vector<std::string> words = explode_string(str, {" ", "-", "\t"});
+ size_t current_line_length = 0;
+ std::string result;
+
+ for (size_t i = 0; i < words.size(); ++i) {
+ std::string word = words[i];
+ // If adding the new word to the current line would be too long,
+ // then put it on a new line (and split it up if it's too long).
+ if (current_line_length +
+ get_sequence_length(word, locale, is_multi_byte_character_support_enabled) >
+ width) {
+ // Only move down to a new line if we have text on the current line.
+ // Avoids situation where wrapped whitespace causes emptylines in text.
+ if (current_line_length > 0) {
+ result += '\n';
+ current_line_length = 0;
+ }
+
+ // If the current word is too long to fit on a line even on it's own then
+ // split the word up.
+ while (get_sequence_length(word, locale, is_multi_byte_character_support_enabled) > width) {
+ result += word.substr(0, width - 1) + "-";
+ word = word.substr(width - 1);
+ result += '\n';
+ }
+
+ // Remove leading whitespace from the word so the new line starts flush to the left.
+ word = trim_left(word);
+ }
+ result += word;
+ current_line_length +=
+ get_sequence_length(word, locale, is_multi_byte_character_support_enabled);
+ }
+ return result;
+ }
+
+ static std::vector<std::string> split_lines(const std::string &text, const std::string &delimiter,
+ const std::string &locale,
+ bool is_multi_byte_character_support_enabled) {
+ std::vector<std::string> result{};
+ std::string input = text;
+ size_t pos = 0;
+ std::string token;
+ while ((pos = input.find(delimiter)) != std::string::npos) {
+ token = input.substr(0, pos);
+ result.push_back(token);
+ input.erase(0, pos + delimiter.length());
+ }
+ if (get_sequence_length(input, locale, is_multi_byte_character_support_enabled))
+ result.push_back(input);
+ return result;
+ };
+
+ // Merge two formats
+ // first has higher precedence
+ // e.g., first = cell-level formatting and
+ // second = row-level formatting
+ // Result has attributes of both with cell-level
+ // formatting taking precedence
+ static Format merge(Format first, Format second) {
+ Format result;
+
+ // Width and height
+ if (first.width_.has_value())
+ result.width_ = first.width_;
+ else
+ result.width_ = second.width_;
+
+ if (first.height_.has_value())
+ result.height_ = first.height_;
+ else
+ result.height_ = second.height_;
+
+ // Font styling
+ if (first.font_align_.has_value())
+ result.font_align_ = first.font_align_;
+ else
+ result.font_align_ = second.font_align_;
+
+ if (first.font_style_.has_value()) {
+ // Merge font styles using std::set_union
+ std::vector<FontStyle> merged_font_style(first.font_style_->size() +
+ second.font_style_->size());
+#if defined(_WIN32) || defined(_WIN64)
+ // Fixes error in Windows - Sequence not ordered
+ std::sort(first.font_style_->begin(), first.font_style_->end());
+ std::sort(second.font_style_->begin(), second.font_style_->end());
+#endif
+ std::set_union(first.font_style_->begin(), first.font_style_->end(),
+ second.font_style_->begin(), second.font_style_->end(),
+ merged_font_style.begin());
+ result.font_style_ = merged_font_style;
+ } else
+ result.font_style_ = second.font_style_;
+
+ if (first.font_color_.has_value())
+ result.font_color_ = first.font_color_;
+ else
+ result.font_color_ = second.font_color_;
+
+ if (first.font_background_color_.has_value())
+ result.font_background_color_ = first.font_background_color_;
+ else
+ result.font_background_color_ = second.font_background_color_;
+
+ // Padding
+ if (first.padding_left_.has_value())
+ result.padding_left_ = first.padding_left_;
+ else
+ result.padding_left_ = second.padding_left_;
+
+ if (first.padding_top_.has_value())
+ result.padding_top_ = first.padding_top_;
+ else
+ result.padding_top_ = second.padding_top_;
+
+ if (first.padding_right_.has_value())
+ result.padding_right_ = first.padding_right_;
+ else
+ result.padding_right_ = second.padding_right_;
+
+ if (first.padding_bottom_.has_value())
+ result.padding_bottom_ = first.padding_bottom_;
+ else
+ result.padding_bottom_ = second.padding_bottom_;
+
+ // Border
+ if (first.border_left_.has_value())
+ result.border_left_ = first.border_left_;
+ else
+ result.border_left_ = second.border_left_;
+
+ if (first.border_left_color_.has_value())
+ result.border_left_color_ = first.border_left_color_;
+ else
+ result.border_left_color_ = second.border_left_color_;
+
+ if (first.border_left_background_color_.has_value())
+ result.border_left_background_color_ = first.border_left_background_color_;
+ else
+ result.border_left_background_color_ = second.border_left_background_color_;
+
+ if (first.border_top_.has_value())
+ result.border_top_ = first.border_top_;
+ else
+ result.border_top_ = second.border_top_;
+
+ if (first.border_top_color_.has_value())
+ result.border_top_color_ = first.border_top_color_;
+ else
+ result.border_top_color_ = second.border_top_color_;
+
+ if (first.border_top_background_color_.has_value())
+ result.border_top_background_color_ = first.border_top_background_color_;
+ else
+ result.border_top_background_color_ = second.border_top_background_color_;
+
+ if (first.border_bottom_.has_value())
+ result.border_bottom_ = first.border_bottom_;
+ else
+ result.border_bottom_ = second.border_bottom_;
+
+ if (first.border_bottom_color_.has_value())
+ result.border_bottom_color_ = first.border_bottom_color_;
+ else
+ result.border_bottom_color_ = second.border_bottom_color_;
+
+ if (first.border_bottom_background_color_.has_value())
+ result.border_bottom_background_color_ = first.border_bottom_background_color_;
+ else
+ result.border_bottom_background_color_ = second.border_bottom_background_color_;
+
+ if (first.border_right_.has_value())
+ result.border_right_ = first.border_right_;
+ else
+ result.border_right_ = second.border_right_;
+
+ if (first.border_right_color_.has_value())
+ result.border_right_color_ = first.border_right_color_;
+ else
+ result.border_right_color_ = second.border_right_color_;
+
+ if (first.border_right_background_color_.has_value())
+ result.border_right_background_color_ = first.border_right_background_color_;
+ else
+ result.border_right_background_color_ = second.border_right_background_color_;
+
+ if (first.show_border_top_.has_value())
+ result.show_border_top_ = first.show_border_top_;
+ else
+ result.show_border_top_ = second.show_border_top_;
+
+ if (first.show_border_bottom_.has_value())
+ result.show_border_bottom_ = first.show_border_bottom_;
+ else
+ result.show_border_bottom_ = second.show_border_bottom_;
+
+ if (first.show_border_left_.has_value())
+ result.show_border_left_ = first.show_border_left_;
+ else
+ result.show_border_left_ = second.show_border_left_;
+
+ if (first.show_border_right_.has_value())
+ result.show_border_right_ = first.show_border_right_;
+ else
+ result.show_border_right_ = second.show_border_right_;
+
+ // Corner
+ if (first.corner_top_left_.has_value())
+ result.corner_top_left_ = first.corner_top_left_;
+ else
+ result.corner_top_left_ = second.corner_top_left_;
+
+ if (first.corner_top_left_color_.has_value())
+ result.corner_top_left_color_ = first.corner_top_left_color_;
+ else
+ result.corner_top_left_color_ = second.corner_top_left_color_;
+
+ if (first.corner_top_left_background_color_.has_value())
+ result.corner_top_left_background_color_ = first.corner_top_left_background_color_;
+ else
+ result.corner_top_left_background_color_ = second.corner_top_left_background_color_;
+
+ if (first.corner_top_right_.has_value())
+ result.corner_top_right_ = first.corner_top_right_;
+ else
+ result.corner_top_right_ = second.corner_top_right_;
+
+ if (first.corner_top_right_color_.has_value())
+ result.corner_top_right_color_ = first.corner_top_right_color_;
+ else
+ result.corner_top_right_color_ = second.corner_top_right_color_;
+
+ if (first.corner_top_right_background_color_.has_value())
+ result.corner_top_right_background_color_ = first.corner_top_right_background_color_;
+ else
+ result.corner_top_right_background_color_ = second.corner_top_right_background_color_;
+
+ if (first.corner_bottom_left_.has_value())
+ result.corner_bottom_left_ = first.corner_bottom_left_;
+ else
+ result.corner_bottom_left_ = second.corner_bottom_left_;
+
+ if (first.corner_bottom_left_color_.has_value())
+ result.corner_bottom_left_color_ = first.corner_bottom_left_color_;
+ else
+ result.corner_bottom_left_color_ = second.corner_bottom_left_color_;
+
+ if (first.corner_bottom_left_background_color_.has_value())
+ result.corner_bottom_left_background_color_ = first.corner_bottom_left_background_color_;
+ else
+ result.corner_bottom_left_background_color_ = second.corner_bottom_left_background_color_;
+
+ if (first.corner_bottom_right_.has_value())
+ result.corner_bottom_right_ = first.corner_bottom_right_;
+ else
+ result.corner_bottom_right_ = second.corner_bottom_right_;
+
+ if (first.corner_bottom_right_color_.has_value())
+ result.corner_bottom_right_color_ = first.corner_bottom_right_color_;
+ else
+ result.corner_bottom_right_color_ = second.corner_bottom_right_color_;
+
+ if (first.corner_bottom_right_background_color_.has_value())
+ result.corner_bottom_right_background_color_ = first.corner_bottom_right_background_color_;
+ else
+ result.corner_bottom_right_background_color_ = second.corner_bottom_right_background_color_;
+
+ // Column separator
+ if (first.column_separator_.has_value())
+ result.column_separator_ = first.column_separator_;
+ else
+ result.column_separator_ = second.column_separator_;
+
+ if (first.column_separator_color_.has_value())
+ result.column_separator_color_ = first.column_separator_color_;
+ else
+ result.column_separator_color_ = second.column_separator_color_;
+
+ if (first.column_separator_background_color_.has_value())
+ result.column_separator_background_color_ = first.column_separator_background_color_;
+ else
+ result.column_separator_background_color_ = second.column_separator_background_color_;
+
+ // Internationlization
+ if (first.multi_byte_characters_.has_value())
+ result.multi_byte_characters_ = first.multi_byte_characters_;
+ else
+ result.multi_byte_characters_ = second.multi_byte_characters_;
+
+ if (first.locale_.has_value())
+ result.locale_ = first.locale_;
+ else
+ result.locale_ = second.locale_;
+
+ return result;
+ }
+
+private:
+ friend class Cell;
+ friend class Row;
+ friend class Column;
+ friend class TableInternal;
+ friend class Printer;
+ friend class MarkdownExporter;
+ friend class LatexExporter;
+ friend class AsciiDocExporter;
+
+ void set_defaults() {
+ // NOTE: width and height are not set here
+ font_align_ = FontAlign::left;
+ font_style_ = std::vector<FontStyle>{};
+ font_color_ = font_background_color_ = Color::none;
+ padding_left_ = padding_right_ = 1;
+ padding_top_ = padding_bottom_ = 0;
+ border_top_ = border_bottom_ = "-";
+ border_left_ = border_right_ = "|";
+ show_border_left_ = show_border_right_ = show_border_top_ = show_border_bottom_ = true;
+ border_top_color_ = border_top_background_color_ = border_bottom_color_ =
+ border_bottom_background_color_ = border_left_color_ = border_left_background_color_ =
+ border_right_color_ = border_right_background_color_ = Color::none;
+ corner_top_left_ = corner_top_right_ = corner_bottom_left_ = corner_bottom_right_ = "+";
+ corner_top_left_color_ = corner_top_left_background_color_ = corner_top_right_color_ =
+ corner_top_right_background_color_ = corner_bottom_left_color_ =
+ corner_bottom_left_background_color_ = corner_bottom_right_color_ =
+ corner_bottom_right_background_color_ = Color::none;
+ column_separator_ = "|";
+ column_separator_color_ = column_separator_background_color_ = Color::none;
+ multi_byte_characters_ = false;
+ locale_ = "";
+ }
+
+ // Helper methods for word wrapping:
+
+ // trim white spaces from the left end of an input string
+ static std::string trim_left(const std::string &input_string) {
+ std::string result = input_string;
+ result.erase(result.begin(), std::find_if(result.begin(), result.end(),
+ [](int ch) { return !std::isspace(ch); }));
+ return result;
+ }
+
+ // trim white spaces from right end of an input string
+ static std::string trim_right(const std::string &input_string) {
+ std::string result = input_string;
+ result.erase(
+ std::find_if(result.rbegin(), result.rend(), [](int ch) { return !std::isspace(ch); })
+ .base(),
+ result.end());
+ return result;
+ }
+
+ // trim white spaces from either end of an input string
+ static std::string trim(const std::string &input_string) {
+ return trim_left(trim_right(input_string));
+ }
+
+ static size_t index_of_any(const std::string &input, size_t start_index,
+ const std::vector<std::string> &split_characters) {
+ std::vector<size_t> indices{};
+ for (auto &c : split_characters) {
+ auto index = input.find(c, start_index);
+ if (index != std::string::npos)
+ indices.push_back(index);
+ }
+ if (indices.size() > 0)
+ return *std::min_element(indices.begin(), indices.end());
+ else
+ return std::string::npos;
+ }
+
+ static std::vector<std::string> explode_string(const std::string &input,
+ const std::vector<std::string> &split_characters) {
+ std::vector<std::string> result{};
+ size_t start_index{0};
+ while (true) {
+ auto index = index_of_any(input, start_index, split_characters);
+
+ if (index == std::string::npos) {
+ result.push_back(input.substr(start_index));
+ return result;
+ }
+
+ std::string word = input.substr(start_index, index - start_index);
+ char next_character = input.substr(index, 1)[0];
+ // Unlike whitespace, dashes and the like should stick to the word occurring before it.
+ if (isspace(next_character)) {
+ result.push_back(word);
+ result.push_back(std::string(1, next_character));
+ } else {
+ result.push_back(word + next_character);
+ }
+ start_index = index + 1;
+ }
+
+ return result;
+ }
+
+ // Element width and height
+ optional<size_t> width_{};
+ optional<size_t> height_{};
+
+ // Font styling
+ optional<FontAlign> font_align_{};
+ optional<std::vector<FontStyle>> font_style_{};
+ optional<Color> font_color_{};
+ optional<Color> font_background_color_{};
+
+ // Element padding
+ optional<size_t> padding_left_{};
+ optional<size_t> padding_top_{};
+ optional<size_t> padding_right_{};
+ optional<size_t> padding_bottom_{};
+
+ // Element border
+ optional<bool> show_border_top_{};
+ optional<std::string> border_top_{};
+ optional<Color> border_top_color_{};
+ optional<Color> border_top_background_color_{};
+
+ optional<bool> show_border_bottom_{};
+ optional<std::string> border_bottom_{};
+ optional<Color> border_bottom_color_{};
+ optional<Color> border_bottom_background_color_{};
+
+ optional<bool> show_border_left_{};
+ optional<std::string> border_left_{};
+ optional<Color> border_left_color_{};
+ optional<Color> border_left_background_color_{};
+
+ optional<bool> show_border_right_{};
+ optional<std::string> border_right_{};
+ optional<Color> border_right_color_{};
+ optional<Color> border_right_background_color_{};
+
+ // Element corner
+ optional<std::string> corner_top_left_{};
+ optional<Color> corner_top_left_color_{};
+ optional<Color> corner_top_left_background_color_{};
+
+ optional<std::string> corner_top_right_{};
+ optional<Color> corner_top_right_color_{};
+ optional<Color> corner_top_right_background_color_{};
+
+ optional<std::string> corner_bottom_left_{};
+ optional<Color> corner_bottom_left_color_{};
+ optional<Color> corner_bottom_left_background_color_{};
+
+ optional<std::string> corner_bottom_right_{};
+ optional<Color> corner_bottom_right_color_{};
+ optional<Color> corner_bottom_right_background_color_{};
+
+ // Element column separator
+ optional<std::string> column_separator_{};
+ optional<Color> column_separator_color_{};
+ optional<Color> column_separator_background_color_{};
+
+ // Internationalization
+ optional<bool> multi_byte_characters_{};
+ optional<std::string> locale_{};
+};
+
+} // namespace tabulate
+
+// #include <tabulate/utf8.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+
+namespace tabulate {
+
+class Cell {
+public:
+ explicit Cell(std::shared_ptr<class Row> parent) : parent_(parent) {}
+
+ void set_text(const std::string &text) { data_ = text; }
+
+ const std::string &get_text() { return data_; }
+
+ size_t size() {
+ return get_sequence_length(data_, locale(), is_multi_byte_character_support_enabled());
+ }
+
+ std::string locale() { return *format().locale_; }
+
+ Format &format();
+
+ bool is_multi_byte_character_support_enabled();
+
+private:
+ std::string data_;
+ std::weak_ptr<class Row> parent_;
+ optional<Format> format_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <iostream>
+#include <memory>
+#include <string>
+// #include <tabulate/cell.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class Row {
+public:
+ explicit Row(std::shared_ptr<class TableInternal> parent) : parent_(parent) {}
+
+ void add_cell(std::shared_ptr<Cell> cell) { cells_.push_back(cell); }
+
+ Cell &operator[](size_t index) { return cell(index); }
+
+ Cell &cell(size_t index) { return *(cells_[index]); }
+
+ std::vector<std::shared_ptr<Cell>> cells() const { return cells_; }
+
+ size_t size() const { return cells_.size(); }
+
+ Format &format();
+
+ class CellIterator {
+ public:
+ explicit CellIterator(std::vector<std::shared_ptr<Cell>>::iterator ptr) : ptr(ptr) {}
+
+ CellIterator operator++() {
+ ++ptr;
+ return *this;
+ }
+ bool operator!=(const CellIterator &other) const { return ptr != other.ptr; }
+ Cell &operator*() { return **ptr; }
+
+ private:
+ std::vector<std::shared_ptr<Cell>>::iterator ptr;
+ };
+
+ auto begin() -> CellIterator { return CellIterator(cells_.begin()); }
+ auto end() -> CellIterator { return CellIterator(cells_.end()); }
+
+private:
+ friend class Printer;
+
+ // Returns the row height as configured
+ // For each cell in the row, check the cell.format.height
+ // property and return the largest configured row height
+ // This is used to ensure that all cells in a row are
+ // aligned when printing the column
+ size_t get_configured_height() {
+ size_t result{0};
+ for (size_t i = 0; i < size(); ++i) {
+ auto cell = cells_[i];
+ auto format = cell->format();
+ if (format.height_.has_value())
+ result = std::max(result, *format.height_);
+ }
+ return result;
+ }
+
+ // Computes the height of the row based on cell contents
+ // and configured cell padding
+ // For each cell, compute:
+ // padding_top + (cell_contents / column height) + padding_bottom
+ // and return the largest value
+ //
+ // This is useful when no cell.format.height is configured
+ // Call get_configured_height()
+ // - If this returns 0, then use get_computed_height()
+ size_t get_computed_height(const std::vector<size_t> &column_widths) {
+ size_t result{0};
+ for (size_t i = 0; i < size(); ++i) {
+ result = std::max(result, get_cell_height(i, column_widths[i]));
+ }
+ return result;
+ }
+
+ // Returns padding_top + cell_contents / column_height + padding_bottom
+ // for a given cell in the column
+ // e.g.,
+ // column width = 5
+ // cell_contents = "I love tabulate" (size/length = 15)
+ // padding top and padding bottom are 1
+ // then, cell height = 1 + (15 / 5) + 1 = 1 + 3 + 1 = 5
+ // The cell will look like this:
+ //
+ // .....
+ // I lov
+ // e tab
+ // ulate
+ // .....
+ size_t get_cell_height(size_t cell_index, size_t column_width) {
+ size_t result{0};
+ Cell &cell = *(cells_[cell_index]);
+ auto format = cell.format();
+ auto text = cell.get_text();
+
+ auto padding_left = *format.padding_left_;
+ auto padding_right = *format.padding_right_;
+
+ result += *format.padding_top_;
+
+ if (column_width > (padding_left + padding_right)) {
+ column_width -= (padding_left + padding_right);
+ }
+
+ // Check if input text has embedded newline characters
+ auto newlines_in_text = std::count(text.begin(), text.end(), '\n');
+ std::string word_wrapped_text;
+ if (newlines_in_text == 0) {
+ // No new lines in input
+ // Apply automatic word wrapping and compute row height
+ word_wrapped_text = Format::word_wrap(text, column_width, cell.locale(),
+ cell.is_multi_byte_character_support_enabled());
+ } else {
+ // There are embedded '\n' characters
+ // Respect these characters
+ word_wrapped_text = text;
+ }
+
+ auto newlines_in_wrapped_text =
+ std::count(word_wrapped_text.begin(), word_wrapped_text.end(), '\n');
+ auto estimated_row_height = newlines_in_wrapped_text;
+
+ if (!word_wrapped_text.empty() &&
+ word_wrapped_text[word_wrapped_text.size() - 1] != '\n') // text doesn't end with a newline
+ estimated_row_height += 1;
+
+ result += estimated_row_height;
+
+ result += *format.padding_bottom_;
+
+ return result;
+ }
+
+ std::vector<std::shared_ptr<Cell>> cells_;
+ std::weak_ptr<class TableInternal> parent_;
+ optional<Format> format_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+class ColumnFormat : public Format {
+public:
+ explicit ColumnFormat(class Column &column) : column_(column) {}
+
+ ColumnFormat &width(size_t value);
+ ColumnFormat &height(size_t value);
+
+ // Padding
+ ColumnFormat &padding(size_t value);
+ ColumnFormat &padding_left(size_t value);
+ ColumnFormat &padding_right(size_t value);
+ ColumnFormat &padding_top(size_t value);
+ ColumnFormat &padding_bottom(size_t value);
+
+ // Border
+ ColumnFormat &border(const std::string &value);
+ ColumnFormat &border_color(Color value);
+ ColumnFormat &border_background_color(Color value);
+ ColumnFormat &border_left(const std::string &value);
+ ColumnFormat &border_left_color(Color value);
+ ColumnFormat &border_left_background_color(Color value);
+ ColumnFormat &border_right(const std::string &value);
+ ColumnFormat &border_right_color(Color value);
+ ColumnFormat &border_right_background_color(Color value);
+ ColumnFormat &border_top(const std::string &value);
+ ColumnFormat &border_top_color(Color value);
+ ColumnFormat &border_top_background_color(Color value);
+ ColumnFormat &border_bottom(const std::string &value);
+ ColumnFormat &border_bottom_color(Color value);
+ ColumnFormat &border_bottom_background_color(Color value);
+
+ // Corner
+ ColumnFormat &corner(const std::string &value);
+ ColumnFormat &corner_color(Color value);
+ ColumnFormat &corner_background_color(Color value);
+
+ // Column separator
+ ColumnFormat &column_separator(const std::string &value);
+ ColumnFormat &column_separator_color(Color value);
+ ColumnFormat &column_separator_background_color(Color value);
+
+ // Font styling
+ ColumnFormat &font_align(FontAlign value);
+ ColumnFormat &font_style(const std::vector<FontStyle> &style);
+ ColumnFormat &font_color(Color value);
+ ColumnFormat &font_background_color(Color value);
+ ColumnFormat &color(Color value);
+ ColumnFormat &background_color(Color value);
+
+ // Locale
+ ColumnFormat &multi_byte_characters(bool value);
+ ColumnFormat &locale(const std::string &value);
+
+private:
+ std::reference_wrapper<class Column> column_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <functional>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string>
+// #include <tabulate/cell.hpp>
+// #include <tabulate/column_format.hpp>
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class Column {
+public:
+ explicit Column(std::shared_ptr<class TableInternal> parent) : parent_(parent) {}
+
+ void add_cell(Cell &cell) { cells_.push_back(cell); }
+
+ Cell &operator[](size_t index) { return cells_[index]; }
+
+ std::vector<std::reference_wrapper<Cell>> cells() const { return cells_; }
+
+ size_t size() const { return cells_.size(); }
+
+ ColumnFormat format() { return ColumnFormat(*this); }
+
+ class CellIterator {
+ public:
+ explicit CellIterator(std::vector<std::reference_wrapper<Cell>>::iterator ptr) : ptr(ptr) {}
+
+ CellIterator operator++() {
+ ++ptr;
+ return *this;
+ }
+ bool operator!=(const CellIterator &other) const { return ptr != other.ptr; }
+ Cell &operator*() { return *ptr; }
+
+ private:
+ std::vector<std::reference_wrapper<Cell>>::iterator ptr;
+ };
+
+ auto begin() -> CellIterator { return CellIterator(cells_.begin()); }
+ auto end() -> CellIterator { return CellIterator(cells_.end()); }
+
+private:
+ friend class ColumnFormat;
+ friend class Printer;
+
+ // Returns the column width as configured
+ // For each cell in the column, check the cell.format.width
+ // property and return the largest configured column width
+ // This is used to ensure that all cells in a column are
+ // aligned when printing the column
+ size_t get_configured_width() {
+ size_t result{0};
+ for (size_t i = 0; i < size(); ++i) {
+ auto cell = cells_[i];
+ auto format = cell.get().format();
+ if (format.width_.has_value())
+ result = std::max(result, *format.width_);
+ }
+ return result;
+ }
+
+ // Computes the width of the column based on cell contents
+ // and configured cell padding
+ // For each cell, compute padding_left + cell_contents + padding_right
+ // and return the largest value
+ //
+ // This is useful when no cell.format.width is configured
+ // Call get_configured_width()
+ // - If this returns 0, then use get_computed_width()
+ size_t get_computed_width() {
+ size_t result{0};
+ for (size_t i = 0; i < size(); ++i) {
+ result = std::max(result, get_cell_width(i));
+ }
+ return result;
+ }
+
+ // Returns padding_left + cell_contents.size() + padding_right
+ // for a given cell in the column
+ size_t get_cell_width(size_t cell_index) {
+ size_t result{0};
+ Cell &cell = cells_[cell_index].get();
+ auto format = cell.format();
+ if (format.padding_left_.has_value())
+ result += *format.padding_left_;
+
+ // Check if input text has newlines
+ auto text = cell.get_text();
+ auto split_lines = Format::split_lines(text, "\n", cell.locale(),
+ cell.is_multi_byte_character_support_enabled());
+
+ // If there are no newlines in input, set column_width = text.size()
+ if (split_lines.size() == 1) {
+ result += cell.size();
+ } else {
+ // There are newlines in input
+ // Find widest substring in input and use this as column_width
+ size_t widest_sub_string_size{0};
+ for (auto &line : split_lines)
+ if (get_sequence_length(line, cell.locale(),
+ cell.is_multi_byte_character_support_enabled()) >
+ widest_sub_string_size)
+ widest_sub_string_size = get_sequence_length(
+ line, cell.locale(), cell.is_multi_byte_character_support_enabled());
+ result += widest_sub_string_size;
+ }
+
+ if (format.padding_right_.has_value())
+ result += *format.padding_right_;
+
+ return result;
+ }
+
+ std::vector<std::reference_wrapper<Cell>> cells_;
+ std::weak_ptr<class TableInternal> parent_;
+};
+
+inline ColumnFormat &ColumnFormat::width(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().width(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::height(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().height(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_left(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding_left(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_right(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding_right(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_top(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding_top(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_bottom(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding_bottom(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_left(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_left_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_left_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_right(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_right_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_right_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_top(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_top_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_top_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_bottom(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_bottom_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_bottom_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().corner(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().corner_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().corner_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().column_separator(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().column_separator_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().column_separator_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_align(FontAlign value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().font_align(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_style(const std::vector<FontStyle> &style) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().font_style(style);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().font_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().font_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::multi_byte_characters(bool value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().multi_byte_characters(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::locale(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().locale(value);
+ return *this;
+}
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/color.hpp>
+// #include <tabulate/font_style.hpp>
+#include <utility>
+#include <vector>
+
+namespace tabulate {
+
+class Printer {
+public:
+ static std::pair<std::vector<size_t>, std::vector<size_t>>
+ compute_cell_dimensions(TableInternal &table);
+
+ static void print_table(std::ostream &stream, TableInternal &table);
+
+ static void print_row_in_cell(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension, size_t num_columns,
+ size_t row_index);
+
+ static bool print_cell_border_top(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension, size_t num_columns);
+ static bool print_cell_border_bottom(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension,
+ size_t num_columns);
+
+ static void apply_element_style(std::ostream &stream, Color foreground_color,
+ Color background_color,
+ const std::vector<FontStyle> &font_style) {
+ apply_foreground_color(stream, foreground_color);
+ apply_background_color(stream, background_color);
+ for (auto &style : font_style)
+ apply_font_style(stream, style);
+ }
+
+ static void reset_element_style(std::ostream &stream) { stream << termcolor::reset; }
+
+private:
+ static void print_content_left_aligned(std::ostream &stream, const std::string &cell_content,
+ const Format &format, size_t text_with_padding_size,
+ size_t column_width) {
+
+ // Apply font style
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+ *format.font_style_);
+ stream << cell_content;
+ // Only apply font_style to the font
+ // Not the padding. So calling apply_element_style with font_style = {}
+ reset_element_style(stream);
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+ if (text_with_padding_size < column_width) {
+ for (size_t j = 0; j < (column_width - text_with_padding_size); ++j) {
+ stream << " ";
+ }
+ }
+ }
+
+ static void print_content_center_aligned(std::ostream &stream, const std::string &cell_content,
+ const Format &format, size_t text_with_padding_size,
+ size_t column_width) {
+ auto num_spaces = column_width - text_with_padding_size;
+ if (num_spaces % 2 == 0) {
+ // Even spacing on either side
+ for (size_t j = 0; j < num_spaces / 2; ++j)
+ stream << " ";
+
+ // Apply font style
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+ *format.font_style_);
+ stream << cell_content;
+ // Only apply font_style to the font
+ // Not the padding. So calling apply_element_style with font_style = {}
+ reset_element_style(stream);
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+ for (size_t j = 0; j < num_spaces / 2; ++j)
+ stream << " ";
+ } else {
+ auto num_spaces_before = num_spaces / 2 + 1;
+ for (size_t j = 0; j < num_spaces_before; ++j)
+ stream << " ";
+
+ // Apply font style
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+ *format.font_style_);
+ stream << cell_content;
+ // Only apply font_style to the font
+ // Not the padding. So calling apply_element_style with font_style = {}
+ reset_element_style(stream);
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+ for (size_t j = 0; j < num_spaces - num_spaces_before; ++j)
+ stream << " ";
+ }
+ }
+
+ static void print_content_right_aligned(std::ostream &stream, const std::string &cell_content,
+ const Format &format, size_t text_with_padding_size,
+ size_t column_width) {
+ if (text_with_padding_size < column_width) {
+ for (size_t j = 0; j < (column_width - text_with_padding_size); ++j) {
+ stream << " ";
+ }
+ }
+
+ // Apply font style
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+ *format.font_style_);
+ stream << cell_content;
+ // Only apply font_style to the font
+ // Not the padding. So calling apply_element_style with font_style = {}
+ reset_element_style(stream);
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+ }
+
+ static void apply_font_style(std::ostream &stream, FontStyle style) {
+ switch (style) {
+ case FontStyle::bold:
+ stream << termcolor::bold;
+ break;
+ case FontStyle::dark:
+ stream << termcolor::dark;
+ break;
+ case FontStyle::italic:
+ stream << termcolor::italic;
+ break;
+ case FontStyle::underline:
+ stream << termcolor::underline;
+ break;
+ case FontStyle::blink:
+ stream << termcolor::blink;
+ break;
+ case FontStyle::reverse:
+ stream << termcolor::reverse;
+ break;
+ case FontStyle::concealed:
+ stream << termcolor::concealed;
+ break;
+ case FontStyle::crossed:
+ stream << termcolor::crossed;
+ break;
+ default:
+ break;
+ }
+ }
+
+ static void apply_foreground_color(std::ostream &stream, Color foreground_color) {
+ switch (foreground_color) {
+ case Color::grey:
+ stream << termcolor::grey;
+ break;
+ case Color::red:
+ stream << termcolor::red;
+ break;
+ case Color::green:
+ stream << termcolor::green;
+ break;
+ case Color::yellow:
+ stream << termcolor::yellow;
+ break;
+ case Color::blue:
+ stream << termcolor::blue;
+ break;
+ case Color::magenta:
+ stream << termcolor::magenta;
+ break;
+ case Color::cyan:
+ stream << termcolor::cyan;
+ break;
+ case Color::white:
+ stream << termcolor::white;
+ break;
+ case Color::none:
+ default:
+ break;
+ }
+ }
+
+ static void apply_background_color(std::ostream &stream, Color background_color) {
+ switch (background_color) {
+ case Color::grey:
+ stream << termcolor::on_grey;
+ break;
+ case Color::red:
+ stream << termcolor::on_red;
+ break;
+ case Color::green:
+ stream << termcolor::on_green;
+ break;
+ case Color::yellow:
+ stream << termcolor::on_yellow;
+ break;
+ case Color::blue:
+ stream << termcolor::on_blue;
+ break;
+ case Color::magenta:
+ stream << termcolor::on_magenta;
+ break;
+ case Color::cyan:
+ stream << termcolor::on_cyan;
+ break;
+ case Color::white:
+ stream << termcolor::on_white;
+ break;
+ case Color::none:
+ default:
+ break;
+ }
+ }
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <iostream>
+#include <string>
+// #include <tabulate/column.hpp>
+// #include <tabulate/font_style.hpp>
+// #include <tabulate/printer.hpp>
+// #include <tabulate/row.hpp>
+// #include <tabulate/termcolor.hpp>
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class TableInternal : public std::enable_shared_from_this<TableInternal> {
+public:
+ static std::shared_ptr<TableInternal> create() {
+ auto result = std::shared_ptr<TableInternal>(new TableInternal());
+ result->format_.set_defaults();
+ return result;
+ }
+
+ void add_row(const std::vector<std::string> &cells) {
+ auto row = std::make_shared<Row>(shared_from_this());
+ for (auto &c : cells) {
+ auto cell = std::make_shared<Cell>(row);
+ cell->set_text(c);
+ row->add_cell(cell);
+ }
+ rows_.push_back(row);
+ }
+
+ Row &operator[](size_t index) { return *(rows_[index]); }
+
+ const Row &operator[](size_t index) const { return *(rows_[index]); }
+
+ Column column(size_t index) {
+ Column column(shared_from_this());
+ for (size_t i = 0; i < rows_.size(); ++i) {
+ auto row = rows_[i];
+ auto &cell = row->cell(index);
+ column.add_cell(cell);
+ }
+ return column;
+ }
+
+ size_t size() const { return rows_.size(); }
+
+ std::pair<size_t, size_t> shape() {
+ std::pair<size_t, size_t> result{0, 0};
+ std::stringstream stream;
+ print(stream);
+ auto buffer = stream.str();
+ auto lines = Format::split_lines(buffer, "\n", "", true);
+ if (lines.size()) {
+ result = {get_sequence_length(lines[0], "", true), lines.size()};
+ }
+ return result;
+ }
+
+ Format &format() { return format_; }
+
+ void print(std::ostream &stream) { Printer::print_table(stream, *this); }
+
+ size_t estimate_num_columns() const {
+ size_t result{0};
+ if (size()) {
+ auto first_row = operator[](size_t(0));
+ result = first_row.size();
+ }
+ return result;
+ }
+
+private:
+ friend class Table;
+ friend class MarkdownExporter;
+
+ TableInternal() {}
+ TableInternal &operator=(const TableInternal &);
+ TableInternal(const TableInternal &);
+
+ std::vector<std::shared_ptr<Row>> rows_;
+ Format format_;
+};
+
+inline Format &Cell::format() {
+ std::shared_ptr<Row> parent = parent_.lock();
+ if (!format_.has_value()) { // no cell format
+ format_ = parent->format(); // Use parent row format
+ } else {
+ // Cell has formatting
+ // Merge cell formatting with parent row formatting
+ format_ = Format::merge(*format_, parent->format());
+ }
+ return *format_;
+}
+
+inline bool Cell::is_multi_byte_character_support_enabled() {
+ return (*format().multi_byte_characters_);
+}
+
+inline Format &Row::format() {
+ std::shared_ptr<TableInternal> parent = parent_.lock();
+ if (!format_.has_value()) { // no row format
+ format_ = parent->format(); // Use parent table format
+ } else {
+ // Row has formatting rules
+ // Merge with parent table format
+ format_ = Format::merge(*format_, parent->format());
+ }
+ return *format_;
+}
+
+inline std::pair<std::vector<size_t>, std::vector<size_t>>
+Printer::compute_cell_dimensions(TableInternal &table) {
+ std::pair<std::vector<size_t>, std::vector<size_t>> result;
+ size_t num_rows = table.size();
+ size_t num_columns = table.estimate_num_columns();
+
+ std::vector<size_t> row_heights, column_widths{};
+
+ for (size_t i = 0; i < num_columns; ++i) {
+ Column column = table.column(i);
+ size_t configured_width = column.get_configured_width();
+ size_t computed_width = column.get_computed_width();
+ if (configured_width != 0)
+ column_widths.push_back(configured_width);
+ else
+ column_widths.push_back(computed_width);
+ }
+
+ for (size_t i = 0; i < num_rows; ++i) {
+ Row row = table[i];
+ size_t configured_height = row.get_configured_height();
+ size_t computed_height = row.get_computed_height(column_widths);
+
+ // NOTE: Unlike column width, row height is calculated as the max
+ // b/w configured height and computed height
+ // which means that .width() has higher precedence than .height()
+ // when both are configured by the user
+ //
+ // TODO: Maybe this can be configured?
+ // If such a configuration is exposed, i.e., prefer height over width
+ // then the logic will be reversed, i.e.,
+ // column_widths.push_back(std::max(configured_width, computed_width))
+ // and
+ // row_height = configured_height if != 0 else computed_height
+
+ row_heights.push_back(std::max(configured_height, computed_height));
+ }
+
+ result.first = row_heights;
+ result.second = column_widths;
+
+ return result;
+}
+
+inline void Printer::print_table(std::ostream &stream, TableInternal &table) {
+ size_t num_rows = table.size();
+ size_t num_columns = table.estimate_num_columns();
+ auto dimensions = compute_cell_dimensions(table);
+ auto row_heights = dimensions.first;
+ auto column_widths = dimensions.second;
+
+ // For each row,
+ for (size_t i = 0; i < num_rows; ++i) {
+
+ // Print top border
+ bool border_top_printed{true};
+ for (size_t j = 0; j < num_columns; ++j) {
+ border_top_printed &= print_cell_border_top(stream, table, {i, j},
+ {row_heights[i], column_widths[j]}, num_columns);
+ }
+ if (border_top_printed)
+ stream << termcolor::reset << "\n";
+
+ // Print row contents with word wrapping
+ for (size_t k = 0; k < row_heights[i]; ++k) {
+ for (size_t j = 0; j < num_columns; ++j) {
+ print_row_in_cell(stream, table, {i, j}, {row_heights[i], column_widths[j]}, num_columns,
+ k);
+ }
+ if (k + 1 < row_heights[i])
+ stream << termcolor::reset << "\n";
+ }
+
+ if (i + 1 == num_rows) {
+
+ // Check if there is bottom border to print:
+ auto bottom_border_needed{true};
+ for (size_t j = 0; j < num_columns; ++j) {
+ auto cell = table[i][j];
+ auto format = cell.format();
+ auto corner = *format.corner_bottom_left_;
+ auto border_bottom = *format.border_bottom_;
+ if (corner == "" && border_bottom == "") {
+ bottom_border_needed = false;
+ break;
+ }
+ }
+
+ if (bottom_border_needed)
+ stream << termcolor::reset << "\n";
+ // Print bottom border for table
+ for (size_t j = 0; j < num_columns; ++j) {
+ print_cell_border_bottom(stream, table, {i, j}, {row_heights[i], column_widths[j]},
+ num_columns);
+ }
+ }
+ if (i + 1 < num_rows)
+ stream << termcolor::reset << "\n"; // Don't add newline after last row
+ }
+}
+
+inline void Printer::print_row_in_cell(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension,
+ size_t num_columns, size_t row_index) {
+ auto column_width = dimension.second;
+ auto cell = table[index.first][index.second];
+ auto locale = cell.locale();
+ auto is_multi_byte_character_support_enabled = cell.is_multi_byte_character_support_enabled();
+ auto old_locale = std::locale::global(std::locale(locale));
+ auto format = cell.format();
+ auto text = cell.get_text();
+ auto word_wrapped_text =
+ Format::word_wrap(text, column_width, locale, is_multi_byte_character_support_enabled);
+ auto text_height = std::count(word_wrapped_text.begin(), word_wrapped_text.end(), '\n') + 1;
+ auto padding_top = *format.padding_top_;
+
+ if (*format.show_border_left_) {
+ apply_element_style(stream, *format.border_left_color_, *format.border_left_background_color_,
+ {});
+ stream << *format.border_left_;
+ reset_element_style(stream);
+ }
+
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+ if (row_index < padding_top) {
+ // Padding top
+ stream << std::string(column_width, ' ');
+ } else if (row_index >= padding_top && (row_index <= (padding_top + text_height))) {
+ // // Row contents
+
+ // Retrieve padding left and right
+ // (column_width - padding_left - padding_right) is the amount of space
+ // available for cell text - Use this to word wrap cell contents
+ auto padding_left = *format.padding_left_;
+ auto padding_right = *format.padding_right_;
+
+ // Check if input text has embedded \n that are to be respected
+ auto newlines_in_input = Format::split_lines(text, "\n", cell.locale(),
+ cell.is_multi_byte_character_support_enabled())
+ .size() -
+ 1;
+ std::string word_wrapped_text;
+
+ // If there are no embedded \n characters, then apply word wrap
+ if (newlines_in_input == 0) {
+ // Apply word wrapping to input text
+ // Then display one word-wrapped line at a time within cell
+ if (column_width > (padding_left + padding_right))
+ word_wrapped_text =
+ Format::word_wrap(text, column_width - padding_left - padding_right, cell.locale(),
+ cell.is_multi_byte_character_support_enabled());
+ else {
+ // Configured column width cannot be lower than (padding_left + padding_right)
+ // This is a bad configuration
+ // E.g., the user is trying to force the column width to be 5
+ // when padding_left and padding_right are each configured to 3
+ // (padding_left + padding_right) = 6 > column_width
+ }
+ } else {
+ word_wrapped_text = text; // repect the embedded '\n' characters
+ }
+
+ auto lines = Format::split_lines(word_wrapped_text, "\n", cell.locale(),
+ cell.is_multi_byte_character_support_enabled());
+
+ if (row_index - padding_top < lines.size()) {
+ auto line = lines[row_index - padding_top];
+
+ // Print left padding characters
+ stream << std::string(padding_left, ' ');
+
+ // Print word-wrapped line
+ line = Format::trim(line);
+ auto line_with_padding_size =
+ get_sequence_length(line, cell.locale(), cell.is_multi_byte_character_support_enabled()) +
+ padding_left + padding_right;
+ switch (*format.font_align_) {
+ case FontAlign::left:
+ print_content_left_aligned(stream, line, format, line_with_padding_size, column_width);
+ break;
+ case FontAlign::center:
+ print_content_center_aligned(stream, line, format, line_with_padding_size, column_width);
+ break;
+ case FontAlign::right:
+ print_content_right_aligned(stream, line, format, line_with_padding_size, column_width);
+ break;
+ }
+
+ // Print right padding characters
+ stream << std::string(padding_right, ' ');
+ } else
+ stream << std::string(column_width, ' ');
+
+ } else {
+ // Padding bottom
+ stream << std::string(column_width, ' ');
+ }
+
+ reset_element_style(stream);
+
+ if (index.second + 1 == num_columns) {
+ // Print right border after last column
+ if (*format.show_border_right_) {
+ apply_element_style(stream, *format.border_right_color_,
+ *format.border_right_background_color_, {});
+ stream << *format.border_right_;
+ reset_element_style(stream);
+ }
+ }
+ std::locale::global(old_locale);
+}
+
+inline bool Printer::print_cell_border_top(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension,
+ size_t num_columns) {
+ auto cell = table[index.first][index.second];
+ auto locale = cell.locale();
+ auto old_locale = std::locale::global(std::locale(locale));
+ auto format = cell.format();
+ auto column_width = dimension.second;
+
+ auto corner = *format.corner_top_left_;
+ auto corner_color = *format.corner_top_left_color_;
+ auto corner_background_color = *format.corner_top_left_background_color_;
+ auto border_top = *format.border_top_;
+
+ if ((corner == "" && border_top == "") || !*format.show_border_top_)
+ return false;
+
+ apply_element_style(stream, corner_color, corner_background_color, {});
+ stream << corner;
+ reset_element_style(stream);
+
+ for (size_t i = 0; i < column_width; ++i) {
+ apply_element_style(stream, *format.border_top_color_, *format.border_top_background_color_,
+ {});
+ stream << border_top;
+ reset_element_style(stream);
+ }
+
+ if (index.second + 1 == num_columns) {
+ // Print corner after last column
+ corner = *format.corner_top_right_;
+ corner_color = *format.corner_top_right_color_;
+ corner_background_color = *format.corner_top_right_background_color_;
+
+ apply_element_style(stream, corner_color, corner_background_color, {});
+ stream << corner;
+ reset_element_style(stream);
+ }
+ std::locale::global(old_locale);
+ return true;
+}
+
+inline bool Printer::print_cell_border_bottom(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension,
+ size_t num_columns) {
+ auto cell = table[index.first][index.second];
+ auto locale = cell.locale();
+ auto old_locale = std::locale::global(std::locale(locale));
+ auto format = cell.format();
+ auto column_width = dimension.second;
+
+ auto corner = *format.corner_bottom_left_;
+ auto corner_color = *format.corner_bottom_left_color_;
+ auto corner_background_color = *format.corner_bottom_left_background_color_;
+ auto border_bottom = *format.border_bottom_;
+
+ if ((corner == "" && border_bottom == "") || !*format.show_border_bottom_)
+ return false;
+
+ apply_element_style(stream, corner_color, corner_background_color, {});
+ stream << corner;
+ reset_element_style(stream);
+
+ for (size_t i = 0; i < column_width; ++i) {
+ apply_element_style(stream, *format.border_bottom_color_,
+ *format.border_bottom_background_color_, {});
+ stream << border_bottom;
+ reset_element_style(stream);
+ }
+
+ if (index.second + 1 == num_columns) {
+ // Print corner after last column
+ corner = *format.corner_bottom_right_;
+ corner_color = *format.corner_bottom_right_color_;
+ corner_background_color = *format.corner_bottom_right_background_color_;
+
+ apply_element_style(stream, corner_color, corner_background_color, {});
+ stream << corner;
+ reset_element_style(stream);
+ }
+ std::locale::global(old_locale);
+ return true;
+}
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/table_internal.hpp>
+
+#if __cplusplus >= 201703L
+#include <string_view>
+#include <variant>
+using std::get_if;
+using std::holds_alternative;
+using std::variant;
+using std::visit;
+using std::string_view;
+#else
+// #include <tabulate/string_view_lite.hpp>
+// #include <tabulate/variant_lite.hpp>
+using nonstd::get_if;
+using nonstd::holds_alternative;
+using nonstd::variant;
+using nonstd::visit;
+using nonstd::string_view;
+#endif
+
+#include <utility>
+
+namespace tabulate {
+
+class Table {
+public:
+ Table() : table_(TableInternal::create()) {}
+
+ using Row_t = std::vector<variant<std::string, const char *, string_view, Table>>;
+
+ Table &add_row(const Row_t &cells) {
+
+ if (rows_ == 0) {
+ // This is the first row added
+ // cells.size() is the number of columns
+ cols_ = cells.size();
+ }
+
+ std::vector<std::string> cell_strings;
+ if (cells.size() < cols_) {
+ cell_strings.resize(cols_);
+ std::fill(cell_strings.begin(), cell_strings.end(), "");
+ } else {
+ cell_strings.resize(cells.size());
+ std::fill(cell_strings.begin(), cell_strings.end(), "");
+ }
+
+ for (size_t i = 0; i < cells.size(); ++i) {
+ auto cell = cells[i];
+ if (holds_alternative<std::string>(cell)) {
+ cell_strings[i] = *get_if<std::string>(&cell);
+ } else if (holds_alternative<const char *>(cell)) {
+ cell_strings[i] = *get_if<const char *>(&cell);
+ } else if (holds_alternative<string_view>(cell)) {
+ cell_strings[i] = std::string{*get_if<string_view>(&cell)};
+ } else {
+ auto table = *get_if<Table>(&cell);
+ std::stringstream stream;
+ table.print(stream);
+ cell_strings[i] = stream.str();
+ }
+ }
+
+ table_->add_row(cell_strings);
+ rows_ += 1;
+ return *this;
+ }
+
+ Row &operator[](size_t index) { return row(index); }
+
+ Row &row(size_t index) { return (*table_)[index]; }
+
+ Column column(size_t index) { return table_->column(index); }
+
+ Format &format() { return table_->format(); }
+
+ void print(std::ostream &stream) { table_->print(stream); }
+
+ std::string str() {
+ std::stringstream stream;
+ print(stream);
+ return stream.str();
+ }
+
+ std::pair<size_t, size_t> shape() { return table_->shape(); }
+
+ class RowIterator {
+ public:
+ explicit RowIterator(std::vector<std::shared_ptr<Row>>::iterator ptr) : ptr(ptr) {}
+
+ RowIterator operator++() {
+ ++ptr;
+ return *this;
+ }
+ bool operator!=(const RowIterator &other) const { return ptr != other.ptr; }
+ Row &operator*() { return **ptr; }
+
+ private:
+ std::vector<std::shared_ptr<Row>>::iterator ptr;
+ };
+
+ auto begin() -> RowIterator { return RowIterator(table_->rows_.begin()); }
+ auto end() -> RowIterator { return RowIterator(table_->rows_.end()); }
+
+private:
+ friend class MarkdownExporter;
+ friend class LatexExporter;
+ friend class AsciiDocExporter;
+
+ friend std::ostream &operator<<(std::ostream &stream, const Table &table);
+ size_t rows_{0};
+ size_t cols_{0};
+ std::shared_ptr<TableInternal> table_;
+};
+
+inline std::ostream &operator<<(std::ostream &stream, const Table &table) {
+ const_cast<Table &>(table).print(stream);
+ return stream;
+}
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <string>
+// #include <tabulate/table.hpp>
+
+namespace tabulate {
+
+class Exporter {
+public:
+ virtual std::string dump(Table &table) = 0;
+ virtual ~Exporter() {}
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/exporter.hpp>
+
+namespace tabulate {
+
+class MarkdownExporter : public Exporter {
+public:
+ std::string dump(Table &table) override {
+ std::string result{""};
+ apply_markdown_format(table);
+ result = table.str();
+ restore_table_format(table);
+ return result;
+ }
+
+ virtual ~MarkdownExporter() {}
+
+private:
+ void add_alignment_header_row(Table &table) {
+ auto &rows = table.table_->rows_;
+
+ if (rows.size() >= 1) {
+ auto alignment_row = std::make_shared<Row>(table.table_->shared_from_this());
+
+ // Create alignment header cells
+ std::vector<std::string> alignment_cells{};
+ for (auto &cell : table[0]) {
+ auto format = cell.format();
+ if (format.font_align_.value() == FontAlign::left) {
+ alignment_cells.push_back(":----");
+ } else if (format.font_align_.value() == FontAlign::center) {
+ alignment_cells.push_back(":---:");
+ } else if (format.font_align_.value() == FontAlign::right) {
+ alignment_cells.push_back("----:");
+ }
+ }
+
+ // Add alignment header cells to alignment row
+ for (auto &c : alignment_cells) {
+ auto cell = std::make_shared<Cell>(alignment_row);
+ cell->format()
+ .hide_border_top()
+ .hide_border_bottom()
+ .border_left("|")
+ .border_right("|")
+ .column_separator("|")
+ .corner("|");
+ cell->set_text(c);
+ if (c == ":---:")
+ cell->format().font_align(FontAlign::center);
+ else if (c == "----:")
+ cell->format().font_align(FontAlign::right);
+ alignment_row->add_cell(cell);
+ }
+
+ // Insert alignment header row
+ if (rows.size() > 1)
+ rows.insert(rows.begin() + 1, alignment_row);
+ else
+ rows.push_back(alignment_row);
+ }
+ }
+
+ void remove_alignment_header_row(Table &table) {
+ auto &rows = table.table_->rows_;
+ table.table_->rows_.erase(rows.begin() + 1);
+ }
+
+ void apply_markdown_format(Table &table) {
+ // Apply markdown format to cells in each row
+ for (auto row : table) {
+ for (auto &cell : row) {
+ auto format = cell.format();
+ formats_.push_back(format);
+ cell.format()
+ .hide_border_top()
+ .hide_border_bottom()
+ .border_left("|")
+ .border_right("|")
+ .column_separator("|")
+ .corner("|");
+ }
+ }
+ // Add alignment header row at position 1
+ add_alignment_header_row(table);
+ }
+
+ void restore_table_format(Table &table) {
+ // Remove alignment header row at position 1
+ remove_alignment_header_row(table);
+
+ // Restore original formatting for each cell
+ size_t format_index{0};
+ for (auto row : table) {
+ for (auto &cell : row) {
+ cell.format() = formats_[format_index];
+ format_index += 1;
+ }
+ }
+ }
+
+ std::vector<Format> formats_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/exporter.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+namespace tabulate {
+
+class LatexExporter : public Exporter {
+
+ static const char new_line = '\n';
+
+public:
+ class ExportOptions {
+ public:
+ ExportOptions &indentation(std::size_t value) {
+ indentation_ = value;
+ return *this;
+ }
+
+ private:
+ friend class LatexExporter;
+ optional<size_t> indentation_;
+ };
+
+ ExportOptions &configure() { return options_; }
+
+ std::string dump(Table &table) override {
+ std::string result{"\\begin{tabular}"};
+ result += new_line;
+
+ result += add_alignment_header(table);
+ result += new_line;
+ const auto rows = table.rows_;
+ // iterate content and put text into the table.
+ for (size_t i = 0; i < rows; i++) {
+ auto &row = table[i];
+ // apply row content indentation
+ if (options_.indentation_.has_value()) {
+ result += std::string(options_.indentation_.value(), ' ');
+ }
+
+ for (size_t j = 0; j < row.size(); j++) {
+
+ result += row[j].get_text();
+
+ // check column position, need "\\" at the end of each row
+ if (j < row.size() - 1) {
+ result += " & ";
+ } else {
+ result += " \\\\";
+ }
+ }
+ result += new_line;
+ }
+
+ result += "\\end{tabular}";
+ return result;
+ }
+
+ virtual ~LatexExporter() {}
+
+private:
+ std::string add_alignment_header(Table &table) {
+ std::string result{"{"};
+
+ for (auto &cell : table[0]) {
+ auto format = cell.format();
+ if (format.font_align_.value() == FontAlign::left) {
+ result += 'l';
+ } else if (format.font_align_.value() == FontAlign::center) {
+ result += 'c';
+ } else if (format.font_align_.value() == FontAlign::right) {
+ result += 'r';
+ }
+ }
+
+ result += "}";
+ return result;
+ }
+ ExportOptions options_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <optional>
+#include <sstream>
+#include <string>
+// #include <tabulate/exporter.hpp>
+
+namespace tabulate {
+
+class AsciiDocExporter : public Exporter {
+
+ static const char new_line = '\n';
+
+public:
+ std::string dump(Table &table) override {
+ std::stringstream ss;
+ ss << add_alignment_header(table);
+ ss << new_line;
+
+ const auto rows = table.rows_;
+ // iterate content and put text into the table.
+ for (size_t row_index = 0; row_index < rows; row_index++) {
+ auto &row = table[row_index];
+
+ for (size_t cell_index = 0; cell_index < row.size(); cell_index++) {
+ ss << "|";
+ ss << add_formatted_cell(row[cell_index]);
+ }
+ ss << new_line;
+ if (row_index == 0) {
+ ss << new_line;
+ }
+ }
+
+ ss << "|===";
+ return ss.str();
+ }
+
+ virtual ~AsciiDocExporter() {}
+
+private:
+ std::string add_formatted_cell(Cell &cell) const {
+ std::stringstream ss;
+ auto format = cell.format();
+ std::string cell_string = cell.get_text();
+
+ auto font_style = format.font_style_.value();
+
+ bool format_bold = false;
+ bool format_italic = false;
+ std::for_each(font_style.begin(), font_style.end(), [&](FontStyle &style) {
+ if (style == FontStyle::bold) {
+ format_bold = true;
+ } else if (style == FontStyle::italic) {
+ format_italic = true;
+ }
+ });
+
+ if (format_bold) {
+ ss << '*';
+ }
+ if (format_italic) {
+ ss << '_';
+ }
+
+ ss << cell_string;
+ if (format_italic) {
+ ss << '_';
+ }
+ if (format_bold) {
+ ss << '*';
+ }
+ return ss.str();
+ }
+
+ std::string add_alignment_header(Table &table) {
+ std::stringstream ss;
+ ss << (R"([cols=")");
+
+ size_t column_count = table[0].size();
+ size_t column_index = 0;
+ for (auto &cell : table[0]) {
+ auto format = cell.format();
+
+ if (format.font_align_.value() == FontAlign::left) {
+ ss << '<';
+ } else if (format.font_align_.value() == FontAlign::center) {
+ ss << '^';
+ } else if (format.font_align_.value() == FontAlign::right) {
+ ss << '>';
+ }
+
+ ++column_index;
+ if (column_index != column_count) {
+ ss << ",";
+ }
+ }
+
+ ss << R"("])";
+ ss << new_line;
+ ss << "|===";
+
+ return ss.str();
+ }
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#ifndef TABULATE_EXPORT_HPP
+#define TABULATE_EXPORT_HPP
+
+// #ifdef _WIN32
+// #ifdef TABULATE_STATIC_LIB
+// #define TABULATE_API
+// #else
+// #ifdef TABULATE_EXPORTS
+// #define TABULATE_API __declspec(dllexport)
+// #else
+// #define TABULATE_API __declspec(dllimport)
+// #endif
+// #endif
+// #else
+// #define TABULATE_API
+// #endif
+
+// Project version
+#define TABULATE_VERSION_MAJOR 1
+#define TABULATE_VERSION_MINOR 4
+#define TABULATE_VERSION_PATCH 0
+
+// Composing the protocol version string from major, and minor
+#define TABULATE_CONCATENATE(A, B) TABULATE_CONCATENATE_IMPL(A, B)
+#define TABULATE_CONCATENATE_IMPL(A, B) A##B
+#define TABULATE_STRINGIFY(a) TABULATE_STRINGIFY_IMPL(a)
+#define TABULATE_STRINGIFY_IMPL(a) #a
+
+#endif
--- /dev/null
+/*
+** Copyright (C) 2017-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This library is free software; you can redistribute it and/or
+** modify it under the terms of the GNU Lesser General Public License
+** as published by the Free Software Foundation; either version 2.1
+** of the License, or (at your option) any later version.
+**
+** This library is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+** Lesser General Public License for more details.
+**
+** You should have received a copy of the GNU Lesser General Public
+** License along with this library; if not, write to the Free
+** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+** 02110-1301, USA.
+*/
+
+#include <string>
+#include <iostream>
+
+#include "mu-tokenizer.hh"
+
+int
+main(int argc, char* argv[])
+{
+ std::string s;
+
+ for (auto i = 1; i < argc; ++i)
+ s += " " + std::string(argv[i]);
+
+ const auto tvec = Mu::tokenize(s);
+ for (const auto& t : tvec)
+ std::cout << t << std::endl;
+
+ return 0;
+}
--- /dev/null
+## 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.
+
+include $(top_srcdir)/gtest.mk
+
+AM_CFLAGS= \
+ $(WARN_CFLAGS) \
+ $(GLIB_CFLAGS) \
+ $(ASAN_CFLAGS) \
+ $(CODE_COVERAGE_CFLAGS) \
+ -DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\" \
+ -DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\" \
+ -Wno-format-nonliteral \
+ -Wno-switch-enum \
+ -Wno-deprecated-declarations \
+ -Wno-inline \
+ -I${top_srcdir}/lib
+
+AM_CPPFLAGS= \
+ $(CODE_COVERAGE_CPPFLAGS)
+
+AM_CXXFLAGS= \
+ $(WARN_CXXFLAGS) \
+ $(GLIB_CFLAGS) \
+ $(ASAN_CXXFLAGS) \
+ $(CODE_COVERAGE_CFLAGS) \
+ -I${top_srcdir}/lib
+
+AM_LDFLAGS= \
+ $(ASAN_LDFLAGS)
+
+noinst_LTLIBRARIES= \
+ libmu-utils.la
+
+libmu_utils_la_SOURCES= \
+ mu-async-queue.hh \
+ mu-command-parser.cc \
+ mu-command-parser.hh \
+ mu-error.hh \
+ mu-logger.cc \
+ mu-logger.hh \
+ mu-option.hh \
+ mu-option.cc \
+ mu-readline.cc \
+ mu-readline.hh \
+ mu-result.hh \
+ mu-sexp.cc \
+ mu-sexp.hh \
+ mu-util.c \
+ mu-util.h \
+ mu-utils.cc \
+ mu-utils.hh \
+ mu-utils-format.hh \
+ mu-xapian-utils.hh
+
+libmu_utils_la_LIBADD= \
+ $(GLIB_LIBS) \
+ $(READLINE_LIBS) \
+ $(CODE_COVERAGE_LIBS)
+
+include $(top_srcdir)/aminclude_static.am
--- /dev/null
+## 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_utils=static_library('mu-utils', [
+ 'mu-command-parser.cc',
+ 'mu-logger.cc',
+ 'mu-option.cc',
+ 'mu-readline.cc',
+ 'mu-sexp.cc',
+ 'mu-test-utils.cc',
+ 'mu-util.c',
+ 'mu-util.h',
+ 'mu-utils.cc'],
+ dependencies: [
+ glib_dep,
+ gio_dep,
+ config_h_dep,
+ readline_dep
+ ],
+ include_directories: include_directories(['.','..']),
+ install: false)
+
+lib_mu_utils_dep = declare_dependency(
+ link_with: lib_mu_utils,
+ include_directories: include_directories(['.', '..'])
+)
+
+subdir('tests')
--- /dev/null
+/*
+** 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_ASYNC_QUEUE_HH__
+#define __MU_ASYNC_QUEUE_HH__
+
+#include <deque>
+#include <mutex>
+#include <chrono>
+#include <condition_variable>
+
+namespace Mu {
+
+constexpr std::size_t UnlimitedAsyncQueueSize{0};
+
+template <typename ItemType, /**< the type of Item to queue */
+ std::size_t MaxSize = UnlimitedAsyncQueueSize, /**< maximum size for the queue */
+ typename Allocator = std::allocator<ItemType>> /**< allocator the items */
+
+class AsyncQueue {
+ public:
+ using value_type = ItemType;
+ using allocator_type = Allocator;
+ using size_type = std::size_t;
+ using reference = value_type&;
+ using const_reference = const value_type&;
+ using pointer = typename std::allocator_traits<allocator_type>::pointer;
+ using const_pointer = typename std::allocator_traits<allocator_type>::const_pointer;
+
+ using Timeout = std::chrono::steady_clock::duration;
+
+#define LOCKED std::unique_lock<std::mutex> lock(m_);
+
+ /**
+ * Push an item to the end of the queue by moving it
+ *
+ * @param item the item to move to the end of the queue
+ * @param timeout and optional timeout
+ *
+ * @return true if the item was pushed; false otherwise.
+ */
+ bool push(const value_type& item, Timeout timeout = {})
+ {
+ return push(std::move(value_type(item)), timeout);
+ }
+
+ /**
+ * Push an item to the end of the queue by moving it
+ *
+ * @param item the item to move to the end of the queue
+ * @param timeout and optional timeout
+ *
+ * @return true if the item was pushed; false otherwise.
+ */
+ bool push(value_type&& item, Timeout timeout = {})
+ {
+ std::unique_lock lock{m_};
+
+ if (!unlimited()) {
+ const auto rv = cv_full_.wait_for(lock, timeout, [&]() {
+ return !full_unlocked();
+ }) && !full_unlocked();
+ if (!rv)
+ return false;
+ }
+
+ q_.emplace_back(std::move(item));
+ cv_empty_.notify_one();
+
+ return true;
+ }
+
+ /**
+ * Pop an item from the queue
+ *
+ * @param receives the value if the function returns true
+ * @param timeout optional time to wait for an item to become available
+ *
+ * @return true if an item was popped (into val), false otherwise.
+ */
+ bool pop(value_type& val, Timeout timeout = {})
+ {
+ std::unique_lock lock{m_};
+
+ if (timeout != Timeout{}) {
+ const auto rv = cv_empty_.wait_for(lock, timeout, [&]() {
+ return !q_.empty();
+ }) && !q_.empty();
+ if (!rv)
+ return false;
+
+ } else if (q_.empty())
+ return false;
+
+ val = std::move(q_.front());
+ q_.pop_front();
+ cv_full_.notify_one();
+
+ return true;
+ }
+
+ /**
+ * Clear the queue
+ *
+ */
+ void clear()
+ {
+ std::unique_lock lock{m_};
+ q_.clear();
+ cv_full_.notify_one();
+ }
+
+ /**
+ * Size of the queue
+ *
+ *
+ * @return the size
+ */
+ size_type size() const
+ {
+ std::unique_lock lock{m_};
+ return q_.size();
+ }
+
+ /**
+ * Maximum size of the queue if specified through the template
+ * parameter; otherwise the (theoretical) max_size of the inner
+ * container.
+ *
+ * @return the maximum size
+ */
+ size_type max_size() const
+ {
+ if (unlimited())
+ return q_.max_size();
+ else
+ return MaxSize;
+ }
+
+ /**
+ * Is the queue empty?
+ *
+ * @return true or false
+ */
+ bool empty() const
+ {
+ std::unique_lock lock{m_};
+ return q_.empty();
+ }
+
+ /**
+ * Is the queue full? Returns false unless a maximum size was specified
+ * (as a template argument)
+ *
+ * @return true or false.
+ */
+ bool full() const
+ {
+ if (unlimited())
+ return false;
+
+ std::unique_lock lock{m_};
+ return full_unlocked();
+ }
+
+ /**
+ * Is this queue (theoretically) unlimited in size?
+ *
+ * @return true or false
+ */
+ constexpr static bool unlimited() { return MaxSize == UnlimitedAsyncQueueSize; }
+
+private:
+ bool full_unlocked() const { return q_.size() >= max_size(); }
+
+ std::deque<ItemType, Allocator> q_;
+ mutable std::mutex m_;
+ std::condition_variable cv_full_, cv_empty_;
+};
+
+} // namespace Mu
+
+#endif /* __MU_ASYNC_QUEUE_HH__ */
--- /dev/null
+/*
+** 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 "mu-command-parser.hh"
+#include "mu-error.hh"
+#include "mu-utils.hh"
+
+#include <iostream>
+#include <algorithm>
+
+using namespace Mu;
+using namespace Command;
+
+void
+Command::invoke(const Command::CommandMap& cmap, const Sexp& call)
+{
+ if (!call.is_call()) {
+ throw Mu::Error{Error::Code::Command,
+ "expected call-sexpr but got %s",
+ call.to_sexp_string().c_str()};
+ }
+
+ const auto& params{call.list()};
+ const auto cmd_it = cmap.find(params.at(0).value());
+ if (cmd_it == cmap.end())
+ throw Mu::Error{Error::Code::Command,
+ "unknown command in call %s",
+ call.to_sexp_string().c_str()};
+
+ const auto& cinfo{cmd_it->second};
+
+ // all required parameters must be present
+ for (auto&& arg : cinfo.args) {
+ const auto& argname{arg.first};
+ const auto& arginfo{arg.second};
+
+ // calls used keyword-parameters, e.g.
+ // (my-function :bar 1 :cuux "fnorb")
+ // so, we're looking for the odd-numbered parameters.
+ const auto param_it = [&]() -> Sexp::Seq::const_iterator {
+ for (size_t i = 1; i < params.size(); i += 2)
+ if (params.at(i).is_symbol() && params.at(i).value() == argname)
+ return params.begin() + i + 1;
+
+ return params.end();
+ }();
+
+ // it's an error when a required parameter is missing.
+ if (param_it == params.end()) {
+ if (arginfo.required)
+ throw Mu::Error{Error::Code::Command,
+ "missing required parameter %s in call %s",
+ argname.c_str(),
+ call.to_sexp_string().c_str()};
+ continue; // not required
+ }
+
+ // the types must match, but the 'nil' symbol is acceptable as
+ // "no value"
+ if (param_it->type() != arginfo.type && !(param_it->is_nil()))
+ throw Mu::Error{Error::Code::Command,
+ "parameter %s expects type %s, but got %s in call %s",
+ argname.c_str(),
+ to_string(arginfo.type).c_str(),
+ to_string(param_it->type()).c_str(),
+ call.to_sexp_string().c_str()};
+ }
+
+ // all passed parameters must be known
+ for (size_t i = 1; i < params.size(); i += 2) {
+ if (std::none_of(cinfo.args.begin(), cinfo.args.end(), [&](auto&& arg) {
+ return params.at(i).value() == arg.first;
+ }))
+ throw Mu::Error{Error::Code::Command,
+ "unknown parameter %s in call %s",
+ params.at(i).value().c_str(),
+ call.to_sexp_string().c_str()};
+ }
+
+ if (cinfo.handler)
+ cinfo.handler(params);
+}
+
+static Sexp::Seq::const_iterator
+find_param_node(const Parameters& params, const std::string& argname)
+{
+ if (params.empty())
+ throw Error(Error::Code::InvalidArgument, "params must not be empty");
+
+ if (argname.empty() || argname.at(0) != ':')
+ throw Error(Error::Code::InvalidArgument,
+ "property key must start with ':' but got '%s')",
+ argname.c_str());
+
+ for (size_t i = 1; i < params.size(); i += 2) {
+ if (i + 1 != params.size() && params.at(i).is_symbol() &&
+ params.at(i).value() == argname)
+ return params.begin() + i + 1;
+ }
+
+ return params.end();
+}
+
+static Error
+wrong_type(Sexp::Type expected, Sexp::Type got)
+{
+ return Error(Error::Code::InvalidArgument,
+ "expected <%s> but got <%s>",
+ to_string(expected).c_str(),
+ to_string(got).c_str());
+}
+
+Option<std::string>
+Command::get_string(const Parameters& params, const std::string& argname)
+{
+ const auto it = find_param_node(params, argname);
+ if (it == params.end() || it->is_nil())
+ return Nothing;
+ else if (!it->is_string())
+ throw wrong_type(Sexp::Type::String, it->type());
+ else
+ return it->value();
+}
+
+Option<std::string>
+Command::get_symbol(const Parameters& params, const std::string& argname)
+{
+ const auto it = find_param_node(params, argname);
+ if (it == params.end() || it->is_nil())
+ return Nothing;
+ else if (!it->is_symbol())
+ throw wrong_type(Sexp::Type::Symbol, it->type());
+ else
+ return it->value();
+}
+
+Option<int>
+Command::get_int(const Parameters& params, const std::string& argname)
+{
+ const auto it = find_param_node(params, argname);
+ if (it == params.end() || it->is_nil())
+ return Nothing;
+ else if (!it->is_number())
+ throw wrong_type(Sexp::Type::Number, it->type());
+ else
+ return ::atoi(it->value().c_str());
+}
+
+Option<unsigned>
+Command::get_unsigned(const Parameters& params, const std::string& argname)
+{
+ if (auto val = get_int(params, argname); val && *val >= 0)
+ return val;
+ else
+ return Nothing;
+}
+
+
+Option<bool>
+Command::get_bool(const Parameters& params, const std::string& argname)
+{
+ const auto it = find_param_node(params, argname);
+ if (it == params.end())
+ return Nothing;
+ else if (!it->is_symbol())
+ throw wrong_type(Sexp::Type::Symbol, it->type());
+ else
+ return it->is_nil() ? false : true;
+}
+
+std::vector<std::string>
+Command::get_string_vec(const Parameters& params, const std::string& argname)
+{
+ const auto it = find_param_node(params, argname);
+ if (it == params.end() || it->is_nil())
+ return {};
+ else if (!it->is_list())
+ throw wrong_type(Sexp::Type::List, it->type());
+
+ std::vector<std::string> vec;
+ for (const auto& n : it->list()) {
+ if (!n.is_string())
+ throw wrong_type(Sexp::Type::String, n.type());
+ vec.emplace_back(n.value());
+ }
+
+ return vec;
+}
--- /dev/null
+/*
+** 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_COMMAND_PARSER_HH__
+#define MU_COMMAND_PARSER_HH__
+
+#include <vector>
+#include <string>
+#include <ostream>
+#include <stdexcept>
+#include <unordered_map>
+#include <functional>
+#include <algorithm>
+
+#include "utils/mu-error.hh"
+#include "utils/mu-sexp.hh"
+#include "utils/mu-option.hh"
+
+namespace Mu {
+namespace Command {
+
+///
+/// Commands are s-expressions with the follow properties:
+
+/// 1) a command is a list with a command-name as its first argument
+/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some
+/// type (ie. 'keyword arguments')
+/// 3) each command is described by its CommandInfo structure, which defines the type
+/// 4) calls to the command must include all required parameters
+/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed
+/// for specify a non-required parameter to be absent; this is for convenience on the
+/// call side.
+
+/// Information about a function argument
+struct ArgInfo {
+ ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg)
+ : type{typearg}, required{requiredarg}, docstring{std::move(docarg)}
+ {
+ }
+ const Sexp::Type type; /**< Sexp::Type of the argument */
+ const bool required; /**< Is this argument required? */
+ const std::string docstring; /**< Documentation */
+};
+
+/// The arguments for a function, which maps their names to the information.
+using ArgMap = std::unordered_map<std::string, ArgInfo>;
+// The parameters to a Handler.
+using Parameters = Sexp::Seq;
+
+Option<int> get_int(const Parameters& parms, const std::string& argname);
+Option<unsigned> get_unsigned(const Parameters& parms, const std::string& argname);
+Option<bool> get_bool(const Parameters& parms, const std::string& argname);
+Option<std::string> get_string(const Parameters& parms, const std::string& argname);
+Option<std::string> get_symbol(const Parameters& parms, const std::string& argname);
+
+std::vector<std::string> get_string_vec(const Parameters& params, const std::string& argname);
+
+/*
+ * backward compat
+ */
+static inline int
+get_int_or(const Parameters& parms, const std::string& arg, int alt = 0) {
+ return get_int(parms, arg).value_or(alt);
+}
+
+static inline bool
+get_bool_or(const Parameters& parms, const std::string& arg, bool alt = false) {
+ return get_bool(parms, arg).value_or(alt);
+}
+static inline std::string
+get_string_or(const Parameters& parms, const std::string& arg, const std::string& alt = ""){
+ return get_string(parms, arg).value_or(alt);
+}
+
+static inline std::string
+get_symbol_or(const Parameters& parms, const std::string& arg, const std::string& alt = "nil") {
+ return get_symbol(parms, arg).value_or(alt);
+}
+
+
+
+
+// A handler function
+using Handler = std::function<void(const Parameters&)>;
+
+/// Information about some command
+struct CommandInfo {
+ CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg)
+ : args{std::move(argmaparg)}, docstring{std::move(docarg)}, handler{
+ std::move(handlerarg)}
+ {
+ }
+ const ArgMap args;
+ const std::string docstring;
+ const Handler handler;
+
+ /**
+ * Get a sorted list of argument names, for display. Required args come
+ * first, then alphabetical.
+ *
+ * @return vec with the sorted names.
+ */
+ std::vector<std::string> sorted_argnames() const
+ { // sort args -- by required, then alphabetical.
+ std::vector<std::string> names;
+ for (auto&& arg : args)
+ names.emplace_back(arg.first);
+ std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) {
+ const auto& arg1{args.find(name1)->second};
+ const auto& arg2{args.find(name2)->second};
+ if (arg1.required != arg2.required)
+ return arg1.required;
+ else
+ return name1 < name2;
+ });
+ return names;
+ }
+};
+/// All commands, mapping their name to information about them.
+using CommandMap = std::unordered_map<std::string, CommandInfo>;
+
+/**
+ * Validate that the call (a Sexp) specifies a valid call, then invoke it.
+ *
+ * A call uses keyword arguments, e.g. something like:
+ * (foo :bar 1 :cuux "fnorb")
+ *
+ * On error, throw Error.
+ *
+ * @param cmap map of commands
+ * @param call node describing a call.
+ */
+void invoke(const Command::CommandMap& cmap, const Sexp& call);
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Command::ArgInfo& info)
+{
+ os << info.type << " (" << (info.required ? "required" : "optional") << ")";
+
+ return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Command::CommandInfo& info)
+{
+ for (auto&& arg : info.args)
+ os << " " << arg.first << " " << arg.second << '\n'
+ << " " << arg.second.docstring << "\n";
+
+ return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Command::CommandMap& map)
+{
+ for (auto&& c : map)
+ os << c.first << '\n' << c.second;
+
+ return os;
+}
+
+} // namespace Command
+} // namespace Mu
+
+#endif /* MU_COMMAND_PARSER_HH__ */
--- /dev/null
+/*
+** 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.
+**
+*/
+
+#ifndef MU_ERROR_HH__
+#define MU_ERROR_HH__
+
+#include <stdexcept>
+#include "mu-utils-format.hh"
+#include "mu-util.h"
+#include <glib.h>
+
+namespace Mu {
+
+struct Error final : public std::exception {
+
+ // 16 lower bits are for the error code the next 8 bits is for the return code
+ // upper byte is for flags
+
+ static constexpr uint32_t SoftError = 1 << 23;
+
+#define ERROR_ENUM(RV,CAT) \
+ static_cast<uint32_t>(__LINE__ | ((RV) << 15) | (CAT))
+
+ enum struct Code: uint32_t {
+ AccessDenied = ERROR_ENUM(1,0),
+ AssertionFailure = ERROR_ENUM(1,0),
+ Command = ERROR_ENUM(1,0),
+ ContactNotFound = ERROR_ENUM(2,SoftError),
+ Crypto = ERROR_ENUM(1,0),
+ File = ERROR_ENUM(1,0),
+ Index = ERROR_ENUM(1,0),
+ Internal = ERROR_ENUM(1,0),
+ InvalidArgument = ERROR_ENUM(1,0),
+ Message = ERROR_ENUM(1,0),
+ NoMatches = ERROR_ENUM(4,SoftError),
+ NotFound = ERROR_ENUM(1,0),
+ Parsing = ERROR_ENUM(1,0),
+ Play = ERROR_ENUM(1,0),
+ Query = ERROR_ENUM(1,0),
+ SchemaMismatch = ERROR_ENUM(1,0),
+ Store = ERROR_ENUM(1,0),
+ StoreLock = ERROR_ENUM(19,0),
+ UnverifiedSignature = ERROR_ENUM(1,0),
+ User = ERROR_ENUM(1,0),
+ Xapian = ERROR_ENUM(1,0),
+ };
+
+
+ /**
+ * Construct an error
+ *
+ * @param codearg error-code
+ * #param msgarg the error diecription
+ */
+ Error(Code codearg, const std::string& msgarg) : code_{codearg}, what_{msgarg} {}
+ Error(Code codearg, std::string&& msgarg) : code_{codearg}, what_{std::move(msgarg)} {}
+
+ /**
+ * Build an error from an error-code and a format string
+ *
+ * @param code error-code
+ * @param frm format string
+ * @param ... format parameters
+ *
+ * @return an Error object
+ */
+ __attribute__((format(printf, 3, 0))) Error(Code codearg, const char* frm, ...)
+ : code_{codearg}
+ {
+ va_list args;
+ va_start(args, frm);
+ what_ = vformat(frm, args);
+ va_end(args);
+ }
+
+ Error(Error&& rhs) = default;
+ Error(const Error& rhs) = default;
+
+ /**
+ * Build an error from a GError an error-code and a format string
+ *
+ * @param code error-code
+ * @param gerr a GError or {}, which is consumed
+ * @param frm format string
+ * @param ... format parameters
+ *
+ * @return an Error object
+ */
+ __attribute__((format(printf, 4, 0)))
+ Error(Code codearg, GError** err, const char* frm, ...)
+ : code_{codearg}
+ {
+ va_list args;
+ va_start(args, frm);
+ what_ = vformat(frm, args);
+ va_end(args);
+
+ if (err && *err)
+ what_ += format(": %s", (*err)->message);
+ else
+ what_ += ": something went wrong";
+
+ g_clear_error(err);
+ }
+
+ /**
+ * DTOR
+ *
+ */
+ virtual ~Error() override = default;
+
+ /**
+ * Get the descriptive message.
+ *
+ * @return
+ */
+ virtual const char* what() const noexcept override { return what_.c_str(); }
+
+ /**
+ * Get the error-code for this error
+ *
+ * @return the error-code
+ */
+ Code code() const noexcept { return code_; }
+
+
+ /**
+ * Is this is a 'soft error'?
+ *
+ * @return true or false
+ */
+ constexpr bool is_soft_error() const {
+ return (static_cast<uint32_t>(code_) & SoftError) != 0;
+ }
+
+ constexpr uint8_t exit_code() const {
+ return ((static_cast<uint32_t>(code_) >> 15) & 0xff);
+ }
+
+
+ /**
+ * Fill a GError with the error information
+ *
+ * @param err GError** (or NULL)
+ */
+ void fill_g_error(GError **err) const noexcept{
+ g_set_error(err, MU_ERROR_DOMAIN, static_cast<int>(code_),
+ "%s", what_.c_str());
+ }
+
+private:
+ const Code code_;
+ std::string what_;
+};
+
+} // namespace Mu
+
+#endif /* MU_ERROR_HH__ */
--- /dev/null
+/*
+** 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.
+**
+*/
+
+#define G_LOG_USE_STRUCTURED
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <iostream>
+#include <fstream>
+#include <cstring>
+
+#include "mu-logger.hh"
+
+using namespace Mu;
+
+static bool MuLogInitialized = false;
+static Mu::LogOptions MuLogOptions;
+static std::ofstream MuStream;
+static auto MaxLogFileSize = 1000 * 1024;
+
+static std::string MuLogPath;
+
+static bool
+maybe_open_logfile()
+{
+ if (MuStream.is_open())
+ return true;
+
+ MuStream.open(MuLogPath, std::ios::out | std::ios::app);
+ if (!MuStream.is_open()) {
+ std::cerr << "opening " << MuLogPath << " failed:" << g_strerror(errno)
+ << std::endl;
+ return false;
+ }
+
+ MuStream.sync_with_stdio(false);
+ return true;
+}
+
+static bool
+maybe_rotate_logfile()
+{
+ static unsigned n = 0;
+
+ if (n++ % 1000 != 0)
+ return true;
+
+ GStatBuf statbuf;
+ if (g_stat(MuLogPath.c_str(), &statbuf) == -1 || statbuf.st_size <= MaxLogFileSize)
+ return true;
+
+ const auto old = MuLogPath + ".old";
+ g_unlink(old.c_str()); // opportunistic
+
+ if (MuStream.is_open())
+ MuStream.close();
+
+ if (g_rename(MuLogPath.c_str(), old.c_str()) != 0)
+ std::cerr << "failed to rename " << MuLogPath << " -> " << old.c_str() << ": "
+ << g_strerror(errno) << std::endl;
+
+ return maybe_open_logfile();
+}
+
+static GLogWriterOutput
+log_file(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+ if (!maybe_open_logfile())
+ return G_LOG_WRITER_UNHANDLED;
+
+ char timebuf[22];
+ time_t now{::time(NULL)};
+ ::strftime(timebuf, sizeof(timebuf), "%F %T", ::localtime(&now));
+
+ char* msg = g_log_writer_format_fields(level, fields, n_fields, FALSE);
+ if (msg && msg[0] == '\n') // hmm... seems lines start with '\n'r
+ msg[0] = ' ';
+
+ MuStream << timebuf << ' ' << msg << std::endl;
+
+ g_free(msg);
+
+ return maybe_rotate_logfile() ? G_LOG_WRITER_HANDLED : G_LOG_WRITER_UNHANDLED;
+}
+
+static GLogWriterOutput
+log_stdouterr(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+ return g_log_writer_standard_streams(level, fields, n_fields, user_data);
+}
+
+static GLogWriterOutput
+log_journal(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+ return g_log_writer_journald(level, fields, n_fields, user_data);
+}
+
+void
+Mu::log_init(const std::string& path, Mu::LogOptions opts)
+{
+ if (MuLogInitialized) {
+ g_error("logging is already initialized");
+ return;
+ }
+
+ if (g_getenv("MU_LOG_STDOUTERR"))
+ opts |= LogOptions::StdOutErr;
+
+ MuLogOptions = opts;
+ MuLogPath = path;
+
+ g_log_set_writer_func(
+ [](GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data) {
+ // filter out debug-level messages?
+ if (level == G_LOG_LEVEL_DEBUG &&
+ (none_of(MuLogOptions & Mu::LogOptions::Debug)))
+ return G_LOG_WRITER_HANDLED;
+
+ // log criticals to stdout / err or if asked
+ if (level == G_LOG_LEVEL_CRITICAL ||
+ any_of(MuLogOptions & Mu::LogOptions::StdOutErr)) {
+ log_stdouterr(level, fields, n_fields, user_data);
+ }
+
+ // log to the journal, or, if not available to a file.
+ if (log_journal(level, fields, n_fields, user_data) != G_LOG_WRITER_HANDLED)
+ return log_file(level, fields, n_fields, user_data);
+ else
+ return G_LOG_WRITER_HANDLED;
+ },
+ NULL,
+ NULL);
+
+ g_message("logging initialized; debug: %s, stdout/stderr: %s",
+ any_of(log_get_options() & LogOptions::Debug) ? "yes" : "no",
+ any_of(log_get_options() & LogOptions::StdOutErr) ? "yes" : "no");
+
+ MuLogInitialized = true;
+}
+
+void
+Mu::log_uninit()
+{
+ if (!MuLogInitialized)
+ return;
+
+ if (MuStream.is_open())
+ MuStream.close();
+
+ MuLogInitialized = false;
+}
+
+void
+Mu::log_set_options(Mu::LogOptions opts)
+{
+ MuLogOptions = opts;
+}
+
+Mu::LogOptions
+Mu::log_get_options()
+{
+ return MuLogOptions;
+}
--- /dev/null
+/*
+** 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_LOGGER_HH__
+#define MU_LOGGER_HH__
+
+#include <string>
+#include "utils/mu-utils.hh"
+
+namespace Mu {
+
+/**
+ * Logging options
+ *
+ */
+enum struct LogOptions {
+ None = 0, /**< Nothing specific */
+ StdOutErr = 1 << 1, /**< Log to stdout/stderr */
+ Debug = 1 << 2, /**< Include debug-level logs */
+};
+
+/**
+ * Initialize the logging system. Note that the path is only used if structured
+ * logging fails -- practically, it goes to the file if there's
+ * systemd/journald.
+ *
+ * if the environment variable MU_LOG_STDOUTERR is set, LogOptions::StdoutErr is
+ * implied.
+ *
+ * @param path path to the log file
+ * @param opts logging options
+ */
+void log_init(const std::string& path, LogOptions opts);
+
+/**
+ * Uninitialize the logging system
+ *
+ */
+void log_uninit();
+
+/**
+ * Change the logging options.
+ *
+ * @param opts options
+ */
+void log_set_options(LogOptions opts);
+
+/**
+ * Get the current log options
+ *
+ * @return the log options
+ */
+LogOptions log_get_options();
+
+} // namespace Mu
+MU_ENABLE_BITOPS(Mu::LogOptions);
+
+#endif /* MU_LOGGER_HH__ */
--- /dev/null
+/*
+** 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-option.hh"
+#include <glib.h>
+
+using namespace Mu;
+
+Mu::Option<std::string>
+Mu::to_string_opt_gchar(gchar*&& str)
+{
+ auto res = to_string_opt(str);
+ g_free(str);
+
+ return res;
+}
--- /dev/null
+/*
+ * Created on 2020-11-08 by Dirk-Jan C. Binnema <dbinnema@logitech.com>
+ *
+ * Copyright (c) 2020 Logitech, Inc. All Rights Reserved
+ * This program is a trade secret of LOGITECH, and it is not to be reproduced,
+ * published, disclosed to others, copied, adapted, distributed or displayed
+ * without the prior authorization of LOGITECH.
+ *
+ * Licensee agrees to attach or embed this notice on all copies of the program,
+ * including partial copies or modified versions thereof.
+ *
+ */
+
+#ifndef MU_OPTION__
+#define MU_OPTION__
+
+#include "thirdparty/optional.hpp"
+#include <string>
+
+namespace Mu {
+
+/// Either a value of type T, or None
+template <typename T> using Option = tl::optional<T>;
+
+template <typename T>
+Option<T>
+Some(T&& t)
+{
+ return std::move(t);
+}
+constexpr auto Nothing = tl::nullopt; // 'None' is take already
+
+/**
+ * Maybe create a string from a const char pointer.
+ *
+ * @param str a char pointer or NULL
+ *
+ * @return option with either the string or nothing if str was NULL.
+ */
+Option<std::string>
+static inline to_string_opt(const char* str) {
+ if (str)
+ return std::string{str};
+ else
+ return Nothing;
+}
+
+/**
+ * Like maybe_string that takes a const char*, but additionally,
+ * g_free() the string.
+ *
+ * @param str char pointer or NULL (consumed)
+ *
+ * @return option with either the string or nothing if str was NULL.
+ */
+Option<std::string> to_string_opt_gchar(char*&& str);
+
+
+} // namespace Mu
+#endif /*MU_OPTION__*/
--- /dev/null
+/*
+** 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 "mu-readline.hh"
+#include "config.h"
+
+#include <iostream>
+#include <unistd.h>
+#include <string>
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#ifdef HAVE_LIBREADLINE
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_READLINE_H)
+#include <readline.h>
+#else /* !defined(HAVE_READLINE_H) */
+extern char* readline();
+#endif /* !defined(HAVE_READLINE_H) */
+char* cmdline = NULL;
+#else /* !defined(HAVE_READLINE_READLINE_H) */
+/* no readline */
+#endif /* HAVE_LIBREADLINE */
+
+#ifdef HAVE_READLINE_HISTORY
+#if defined(HAVE_READLINE_HISTORY_H)
+#include <readline/history.h>
+#elif defined(HAVE_HISTORY_H)
+#include <history.h>
+#else /* !defined(HAVE_HISTORY_H) */
+extern void add_history();
+extern int write_history();
+extern int read_history();
+#endif /* defined(HAVE_READLINE_HISTORY_H) */
+/* no history */
+#endif /* HAVE_READLINE_HISTORY */
+
+#if defined(HAVE_LIBREADLINE) && defined(HAVE_READLINE_HISTORY)
+#define HAVE_READLINE (1)
+#else
+#define HAVE_READLINE (0)
+#endif
+
+using namespace Mu;
+
+static bool is_a_tty{};
+static std::string hist_path;
+static size_t max_lines{};
+
+bool
+Mu::have_readline()
+{
+ return HAVE_READLINE != 0;
+}
+
+
+void
+Mu::setup_readline(const std::string& histpath, size_t maxlines)
+{
+ is_a_tty = !!::isatty(::fileno(stdout));
+ hist_path = histpath;
+ max_lines = maxlines;
+
+#if HAVE_READLINE
+ rl_bind_key('\t', rl_insert); // default (filenames) is not useful
+ using_history();
+ read_history(hist_path.c_str());
+
+ if (max_lines > 0)
+ stifle_history(max_lines);
+#endif /*HAVE_READLINE*/
+}
+
+void
+Mu::shutdown_readline()
+{
+#if HAVE_READLINE
+ if (!is_a_tty)
+ return;
+
+ write_history(hist_path.c_str());
+ if (max_lines > 0)
+ history_truncate_file(hist_path.c_str(), max_lines);
+#endif /*HAVE_READLINE*/
+}
+
+std::string
+Mu::read_line(bool& do_quit)
+{
+#if HAVE_READLINE
+ if (is_a_tty) {
+ auto buf = readline(";; mu% ");
+ if (!buf) {
+ do_quit = true;
+ return {};
+ }
+ std::string line{buf};
+ ::free(buf);
+ return line;
+ }
+#endif /*HAVE_READLINE*/
+
+ std::string line;
+ std::cout << ";; mu> ";
+ if (!std::getline(std::cin, line))
+ do_quit = true;
+
+ return line;
+}
+
+void
+Mu::save_line(const std::string& line)
+{
+#if HAVE_READLINE
+ if (is_a_tty)
+ add_history(line.c_str());
+#endif /*HAVE_READLINE*/
+}
--- /dev/null
+/*
+** 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 <string>
+
+namespace Mu {
+
+/**
+ * Setup readline when available and on tty.
+ *
+ * @param histpath path to the history file
+ * @param max_lines maximum number of history to save
+ */
+void setup_readline(const std::string& histpath, size_t max_lines);
+
+/**
+ * Shutdown readline
+ *
+ */
+void shutdown_readline();
+
+/**
+ * Read a command line
+ *
+ * @param do_quit recceives whether we should quit.
+ *
+ * @return the string read or empty
+ */
+std::string read_line(bool& do_quit);
+
+/**
+ * Save a line to history (or do nothing when readline is not active)
+ *
+ * @param line a line.
+ */
+void save_line(const std::string& line);
+
+
+/**
+ * Do we have the non-shim readline?
+ *
+ * @return true or failse
+ */
+bool have_readline();
+
+} // namespace Mu
--- /dev/null
+/*
+** Copyright (C) 2019-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_RESULT_HH__
+#define MU_RESULT_HH__
+
+#include "thirdparty/expected.hpp"
+#include "utils/mu-error.hh"
+
+namespace Mu {
+/**
+ * A little Rust-envy...a Result is _either_ some value of type T, _or_ a Mu::Error
+ */
+template <typename T> using Result = tl::expected<T, Error>;
+
+/**
+ * Ok() is not typically strictly needed (unlike Err), but imitates Rust's Ok
+ * and it helps the reader.
+ *
+ * @param t the value to return
+ *
+ * @return a success Result<T>
+ */
+template <typename T>
+class Result<T>::expected
+// note: "class", not "typename";
+// https://stackoverflow.com/questions/46412754/class-name-injection-and-constructors
+Ok(T&& t)
+{
+ return std::move(t);
+}
+
+/**
+ * Implementation of Ok() for void results.
+ *
+ * @return a success Result<void>
+ */
+static inline Result<void>
+Ok()
+{
+ return {};
+}
+
+/**
+ * Return an error
+ *
+ * @param err the error
+ *
+ * @return error
+ */
+static inline tl::unexpected<Error>
+Err(Error&& err)
+{
+ return tl::unexpected(std::move(err));
+}
+
+static inline tl::unexpected<Error>
+Err(const Error& err)
+{
+ return tl::unexpected(err);
+}
+
+template<typename T>
+static inline tl::unexpected<Error>
+Err(const Result<T>& res)
+{
+ return res.error();
+}
+
+
+
+template <typename T>
+static inline Result<void>
+Ok(const T& t)
+{
+ if (t)
+ return Ok();
+ else
+ return Err(t.error());
+}
+
+
+/*
+ * convenience
+ */
+
+static inline tl::unexpected<Error>
+Err(Error::Code errcode, std::string&& msg="")
+{
+ return Err(Error{errcode, std::move(msg)});
+}
+
+__attribute__((format(printf, 2, 0)))
+static inline tl::unexpected<Error>
+Err(Error::Code errcode, const char* frm, ...)
+{
+ va_list args;
+ va_start(args, frm);
+ auto str{vformat(frm, args)};
+ va_end(args);
+
+ return Err(errcode, std::move(str));
+}
+
+__attribute__((format(printf, 3, 0)))
+static inline tl::unexpected<Error>
+Err(Error::Code errcode, GError **err, const char* frm, ...)
+{
+ va_list args;
+ va_start(args, frm);
+ auto str{vformat(frm, args)};
+ va_end(args);
+
+ if (err && *err)
+ str += format(" (%s)", (*err)->message ? (*err)->message : "");
+ g_clear_error(err);
+
+ return Err(errcode, std::move(str));
+}
+
+
+
+/**
+ * Assert that some result has a value (for unit tests)
+ *
+ * @param R some result
+ */
+#define assert_valid_result(R) do { \
+ if(!R) { \
+ g_printerr("%s:%u: error-result: %s\n", \
+ __FILE__, __LINE__, \
+ (R).error().what()); \
+ g_assert_true(!!R); \
+ } \
+} while(0)
+
+
+
+}// namespace Mu
+
+#endif /* MU_RESULT_HH__ */
--- /dev/null
+/*
+** 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 "mu-sexp.hh"
+#include "mu-utils.hh"
+
+#include <sstream>
+#include <array>
+
+using namespace Mu;
+
+__attribute__((format(printf, 2, 0))) static Mu::Error
+parsing_error(size_t pos, const char* frm, ...)
+{
+ va_list args;
+ va_start(args, frm);
+ auto msg = vformat(frm, args);
+ va_end(args);
+
+ if (pos == 0)
+ return Mu::Error(Error::Code::Parsing, "%s", msg.c_str());
+ else
+ return Mu::Error(Error::Code::Parsing, "%zu: %s", pos, msg.c_str());
+}
+static size_t
+skip_whitespace(const std::string& s, size_t pos)
+{
+ while (pos != s.size()) {
+ if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n')
+ ++pos;
+ else
+ break;
+ }
+
+ return pos;
+}
+
+static Sexp parse(const std::string& expr, size_t& pos);
+
+static Sexp
+parse_list(const std::string& expr, size_t& pos)
+{
+ if (expr[pos] != '(') // sanity check.
+ throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]);
+
+ Sexp::List list;
+
+ ++pos;
+ while (expr[pos] != ')' && pos != expr.size())
+ list.add(parse(expr, pos));
+
+ if (expr[pos] != ')')
+ throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]);
+ ++pos;
+ return Sexp::make_list(std::move(list));
+}
+
+// parse string
+static Sexp
+parse_string(const std::string& expr, size_t& pos)
+{
+ if (expr[pos] != '"') // sanity check.
+ throw parsing_error(pos, "expected: '\"'' but got '%c", expr[pos]);
+
+ bool escape{};
+ std::string str;
+ for (++pos; pos != expr.size(); ++pos) {
+ auto kar = expr[pos];
+ if (escape && (kar == '"' || kar == '\\')) {
+ str += kar;
+ escape = false;
+ continue;
+ }
+
+ if (kar == '"')
+ break;
+ else if (kar == '\\')
+ escape = true;
+ else
+ str += kar;
+ }
+
+ if (escape || expr[pos] != '"')
+ throw parsing_error(pos, "unterminated string '%s'", str.c_str());
+
+ ++pos;
+ return Sexp::make_string(std::move(str));
+}
+
+static Sexp
+parse_integer(const std::string& expr, size_t& pos)
+{
+ if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check.
+ throw parsing_error(pos, "expected: <digit> but got '%c", expr[pos]);
+
+ std::string num; // negative number?
+ if (expr[pos] == '-') {
+ num = "-";
+ ++pos;
+ }
+
+ for (; isdigit(expr[pos]); ++pos)
+ num += expr[pos];
+
+ return Sexp::make_number(::atoi(num.c_str()));
+}
+
+static Sexp
+parse_symbol(const std::string& expr, size_t& pos)
+{
+ if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check.
+ throw parsing_error(pos, "expected: <alpha>|: but got '%c", expr[pos]);
+
+ std::string symbol(1, expr[pos]);
+ for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos)
+ symbol += expr[pos];
+
+ return Sexp::make_symbol(std::move(symbol));
+}
+
+static Sexp
+parse(const std::string& expr, size_t& pos)
+{
+ pos = skip_whitespace(expr, pos);
+
+ if (pos == expr.size())
+ throw parsing_error(pos, "expected: character '%c", expr[pos]);
+
+ const auto kar = expr[pos];
+ const auto node = [&]() -> Sexp {
+ if (kar == '(')
+ return parse_list(expr, pos);
+ else if (kar == '"')
+ return parse_string(expr, pos);
+ else if (isdigit(kar) || kar == '-')
+ return parse_integer(expr, pos);
+ else if (isalpha(kar) || kar == ':')
+ return parse_symbol(expr, pos);
+ else
+ throw parsing_error(pos, "unexpected character '%c", kar);
+ }();
+
+ pos = skip_whitespace(expr, pos);
+
+ return node;
+}
+
+Sexp
+Sexp::make_parse(const std::string& expr)
+{
+ size_t pos{};
+ auto node{::parse(expr, pos)};
+
+ if (pos != expr.size())
+ throw parsing_error(pos, "trailing data starting with '%c'", expr[pos]);
+
+ return node;
+}
+
+std::string
+Sexp::to_sexp_string() const
+{
+ std::stringstream sstrm;
+
+ switch (type()) {
+ case Type::List: {
+ sstrm << '(';
+ bool first{true};
+ for (auto&& child : list()) {
+ sstrm << (first ? "" : " ") << child.to_sexp_string();
+ first = false;
+ }
+ sstrm << ')';
+
+ if (any_of(formatting_opts & FormattingOptions::SplitList))
+ sstrm << '\n';
+ break;
+ }
+ case Type::String:
+ sstrm << quote(value());
+ break;
+ case Type::Raw:
+ sstrm << value();
+ break;
+ case Type::Number:
+ case Type::Symbol:
+ case Type::Empty:
+ default: sstrm << value();
+ }
+
+ return sstrm.str();
+}
+
+// LCOV_EXCL_START
+
+std::string
+Sexp::to_json_string() const
+{
+ std::stringstream sstrm;
+
+ switch (type()) {
+ case Type::List: {
+ // property-lists become JSON objects
+ if (is_prop_list()) {
+ sstrm << "{";
+ auto it{list().begin()};
+ bool first{true};
+ while (it != list().end()) {
+ sstrm << (first ? "" : ",") << quote(it->value()) << ":";
+ ++it;
+ sstrm << it->to_json_string();
+ ++it;
+ first = false;
+ }
+ sstrm << "}";
+ if (any_of(formatting_opts & FormattingOptions::SplitList))
+ sstrm << '\n';
+ } else { // other lists become arrays.
+ sstrm << '[';
+ bool first{true};
+ for (auto&& child : list()) {
+ sstrm << (first ? "" : ", ") << child.to_json_string();
+ first = false;
+ }
+ sstrm << ']';
+ if (any_of(formatting_opts & FormattingOptions::SplitList))
+ sstrm << '\n';
+ }
+ break;
+ }
+ case Type::String:
+ sstrm << quote(value());
+ break;
+ case Type::Raw: // FIXME: implement this.
+ break;
+
+ case Type::Symbol:
+ if (is_nil())
+ sstrm << "false";
+ else if (is_t())
+ sstrm << "true";
+ else
+ sstrm << quote(value());
+ break;
+ case Type::Number:
+ case Type::Empty:
+ default: sstrm << value();
+ }
+
+ return sstrm.str();
+}
+
+// LCOV_EXCL_STOP
--- /dev/null
+/*
+** 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_SEXP_HH__
+#define MU_SEXP_HH__
+
+#include <string>
+#include <vector>
+#include <type_traits>
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-error.hh"
+
+namespace Mu {
+/// Simple s-expression parser & list that parses lists () and atoms (strings
+/// ("-quoted), (positive) integers ([0..9]+) and symbol starting with alpha or
+/// ':', then alphanum and '-')
+///
+/// (:foo (1234 "bar" nil) :quux (a b c))
+
+/// Parse node
+struct Sexp {
+ /// Node type
+ enum struct Type { Empty, List, String, Number, Symbol, Raw };
+
+ /**
+ * Default CTOR
+ */
+ Sexp() : type_{Type::Empty} {}
+
+ // Underlying data type for list; we'd like to use std::dequeu here,
+ // but that does not compile with libc++ (it does with libstdc++)
+ using Seq = std::vector<Sexp>;
+
+ /**
+ * Make a sexp out of an s-expression string.
+ *
+ * @param expr a string containing an s-expression
+ *
+ * @return the parsed s-expression, or throw Error.
+ */
+ static Sexp make_parse(const std::string& expr);
+
+ /**
+ * Make a node for a string/integer/symbol/list value
+ *
+ * @param val some value
+ * @param empty_is_nil turn empty string into a 'nil' symbol
+ *
+ * @return a node
+ */
+ static Sexp make_string(std::string&& val, bool empty_is_nil=false)
+ {
+ if (empty_is_nil && val.empty())
+ return make_symbol("nil");
+ else
+ return Sexp{Type::String, std::move(val)};
+ }
+ static Sexp make_string(const std::string& val, bool empty_is_nil=false)
+ {
+ if (empty_is_nil && val.empty())
+ return make_symbol("nil");
+ else
+ return Sexp{Type::String, std::string(val)};
+ }
+
+ static Sexp make_number(int val) { return Sexp{Type::Number, format("%d", val)}; }
+ static Sexp make_symbol(std::string&& val) {
+ if (val.empty())
+ throw Error(Error::Code::InvalidArgument,
+ "symbol must be non-empty");
+ return Sexp{Type::Symbol, std::move(val)};
+ }
+ static Sexp make_symbol_sv(std::string_view val) {
+ return make_symbol(std::string{val});
+ }
+
+ /**
+ * Add a raw string sexp.
+ *
+ * @param val value
+ *
+ * @return A sexp
+ */
+ static Sexp make_raw(std::string&& val) {
+ return Sexp{Type::Raw, std::string{val}};
+ }
+ static Sexp make_raw(const std::string& val) {
+ return make_raw(std::string{val});
+ }
+
+
+ /**
+ *
+ *
+ * The value of this node; invalid for list nodes.
+ *
+ * @return
+ */
+ const std::string& value() const {
+ if (is_list())
+ throw Error(Error::Code::InvalidArgument, "no value for list");
+ if (is_empty())
+ throw Error{Error::Code::InvalidArgument, "no value for empty"};
+ return value_;
+ }
+
+ /**
+ * The underlying container of this list node; only valid for lists
+ *
+ * @return
+ */
+ const Seq& list() const {
+ if (!is_list())
+ throw Error(Error::Code::InvalidArgument, "not a list");
+ return seq_;
+ }
+
+ /**
+ * Convert a Sexp to its S-expression string representation
+ *
+ * @return the string representation
+ */
+ std::string to_sexp_string() const;
+
+ /**
+ * Convert a Sexp::Node to its JSON string representation
+ *
+ * @return the string representation
+ */
+ std::string to_json_string() const;
+
+ /**
+ * Return the type of this Node.
+ *
+ * @return the type
+ */
+ Type type() const { return type_; }
+
+ ///
+ /// Helper struct to build mutable lists.
+ ///
+ struct List {
+ List () = default;
+ List (const Seq& seq): seq_{seq} {}
+
+ /**
+ * Add a sexp to the list
+ *
+ * @param sexp a sexp
+ * @param args rest arguments
+ *
+ * @return a ref to this List (for chaining)
+ */
+ List& add() { return *this; }
+ List& add(Sexp&& sexp)
+ {
+ seq_.emplace_back(std::move(sexp));
+ return *this;
+ }
+ template <typename... Args> List& add(Sexp&& sexp, Args... args)
+ {
+ seq_.emplace_back(std::move(sexp));
+ seq_.emplace_back(std::forward<Args>(args)...);
+ return *this;
+ }
+
+ /**
+ * Add a property (i.e., :key sexp ) to the list. Remove any
+ * prop with the same name
+ *
+ * @param name a property-name. Must start with ':', length > 1
+ * @param sexp a sexp
+ * @param args rest arguments
+ *
+ * @return a ref to this List (for chaining)
+ */
+ List& add_prop(std::string&& name, Sexp&& sexp) {
+ remove_prop(name);
+ if (!is_prop_name(name))
+ throw Error{Error::Code::InvalidArgument,
+ "invalid property name ('%s')",
+ name.c_str()};
+ seq_.emplace_back(make_symbol(std::move(name)));
+ seq_.emplace_back(std::move(sexp));
+ return *this;
+ }
+ template <typename... Args>
+ List& add_prop(std::string&& name, Sexp&& sexp, Args... args) {
+ remove_prop(name);
+ add_prop(std::move(name), std::move(sexp));
+ add_prop(std::forward<Args>(args)...);
+ return *this;
+ }
+
+ void remove_prop(const std::string& name) {
+ if (!is_prop_name(name))
+ throw Error{Error::Code::InvalidArgument,
+ "invalid property name ('%s')", name.c_str()};
+ auto it = std::find_if(seq_.begin(), seq_.end(), [&](auto&& elm) {
+ return elm.type() == Sexp::Type::Symbol &&
+ elm.value() == name;
+ });
+ if (it != seq_.cend() && it + 1 != seq_.cend()) {
+ /* erase propname and value.*/
+ seq_.erase(it, it + 2);
+ }
+ }
+
+ /**
+ * Remove all elements from the list.
+ */
+ void clear() { seq_.clear(); }
+
+ /**
+ * Get the number of elements in the list
+ *
+ * @return number
+ */
+ size_t size() const { return seq_.size(); }
+
+ /**
+ * Is the list empty?
+ *
+ * @return true or false
+ */
+ size_t empty() const { return seq_.empty(); }
+
+ private:
+ friend struct Sexp;
+ Seq seq_;
+ };
+
+ /**
+ * Construct a list sexp from a List
+ *
+ * @param list a list-list
+ * @param sexp a Sexp
+ * @param args rest arguments
+ *
+ * @return a sexp.
+ */
+ static Sexp make_list(List&& list) { return Sexp{Type::List, std::move(list.seq_)}; }
+ template <typename... Args> static Sexp make_list(Sexp&& sexp, Args... args)
+ {
+ List lst;
+ lst.add(std::move(sexp)).add(std::forward<Args>(args)...);
+ return make_list(std::move(lst));
+ }
+
+ /**
+ * Construct a property list sexp from a List
+ *
+ * @param name the property name; must start wtth ':'
+ * @param sexp a Sexp
+ * @param args rest arguments (property list)
+ *
+ * @return a sexp.
+ */
+ template <typename... Args>
+ static Sexp make_prop_list(std::string&& name, Sexp&& sexp, Args... args)
+ {
+ List list;
+ list.add_prop(std::move(name), std::move(sexp), std::forward<Args>(args)...);
+ return make_list(std::move(list));
+ }
+
+ /**
+ * Construct a properrty list sexp from a List
+ *
+ * @param funcname function name for the call
+ * @param name the property name; must start wtth ':'
+ * @param sexp a Sexp
+ * @param args rest arguments (property list)
+ *
+ * @return a sexp.
+ */
+ template <typename... Args>
+ static Sexp make_call(std::string&& funcname, std::string&& name, Sexp&& sexp, Args... args)
+ {
+ List list;
+ list.add(make_symbol(std::move(funcname)));
+ list.add_prop(std::move(name), std::move(sexp), std::forward<Args>(args)...);
+ return make_list(std::move(list));
+ }
+
+ /// Some type helpers
+ bool is_list() const { return type() == Type::List; }
+ bool is_string() const { return type() == Type::String; }
+ bool is_number() const { return type() == Type::Number; }
+ bool is_symbol() const { return type() == Type::Symbol; }
+ bool is_empty() const { return type() == Type::Empty; }
+
+ operator bool() const { return !is_empty(); }
+
+ static constexpr auto SymbolNil{"nil"};
+ static constexpr auto SymbolT{"t"};
+ bool is_nil() const { return is_symbol() && value() == SymbolNil; }
+ bool is_t() const { return is_symbol() && value() == SymbolT; }
+
+ /**
+ * Is this a prop-list? A prop list is a list sexp with alternating
+ * property / sexp
+ *
+ * @return
+ */
+ bool is_prop_list() const
+ {
+ if (!is_list() || list().size() % 2 != 0)
+ return false;
+ else
+ return is_prop_list(list().begin(), list().end());
+ }
+
+ /**
+ * Is this a call? A call is a list sexp with a symbol (function name),
+ * followed by a prop list
+ *
+ * @return
+ */
+ bool is_call() const
+ {
+ if (!is_list() || list().size() % 2 != 1 || !list().at(0).is_symbol())
+ return false;
+ else
+ return is_prop_list(list().begin() + 1, list().end());
+ }
+
+ enum struct FormattingOptions {
+ Default = 0, /**< Nothing in particular */
+ SplitList = 1 << 0, /**< Insert newline after list item */
+ };
+
+ FormattingOptions formatting_opts{}; /**< Formatting option for the
+ * string output */
+
+private:
+ Sexp(Type typearg, std::string&& valuearg) : type_{typearg}, value_{std::move(valuearg)} {
+ if (is_list())
+ throw Error{Error::Code::InvalidArgument, "cannot be a list type"};
+ if (is_empty())
+ throw Error{Error::Code::InvalidArgument, "cannot be an empty type"};
+ }
+ Sexp(Type typearg, Seq&& seq) : type_{Type::List}, seq_{std::move(seq)} {
+ if (!is_list())
+ throw Error{Error::Code::InvalidArgument, "must be a list type"};
+ if (is_empty())
+ throw Error{Error::Code::InvalidArgument, "cannot be an empty type"};
+ }
+ /**
+ * Is the sexp a valid property name?
+ *
+ * @param sexp a Sexp.
+ *
+ * @return true or false.
+ */
+ static bool is_prop_name(const std::string& str)
+ {
+ return str.size() > 1 && str.at(0) == ':';
+ }
+ static bool is_prop_name(const Sexp& sexp)
+ {
+ return sexp.is_symbol() && is_prop_name(sexp.value());
+ }
+
+ static bool is_prop_list(Seq::const_iterator b, Seq::const_iterator e)
+ {
+ while (b != e) {
+ const Sexp& s{*b};
+ if (!is_prop_name(s))
+ return false;
+ if (++b == e)
+ return false;
+ ++b;
+ }
+ return b == e;
+ }
+
+ Type type_; /**< Type of node */
+ std::string value_; /**< String value of node (only for
+ * non-Type::Lst)*/
+ Seq seq_; /**< Children of node (only for
+ * Type::Lst) */
+};
+
+static inline std::ostream&
+operator<<(std::ostream& os, Sexp::Type id)
+{
+ switch (id) {
+ case Sexp::Type::List:
+ os << "list";
+ break;
+ case Sexp::Type::String:
+ os << "string";
+ break;
+ case Sexp::Type::Number:
+ os << "number";
+ break;
+ case Sexp::Type::Symbol:
+ os << "symbol";
+ break;
+ case Sexp::Type::Raw:
+ os << "raw";
+ break;
+ case Sexp::Type::Empty:
+ os << "empty";
+ break;
+ default: throw std::runtime_error("unknown node type");
+ }
+
+ return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Sexp& sexp)
+{
+ os << sexp.to_sexp_string();
+ return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Sexp::List& sexp)
+{
+ os << Sexp::make_list(Sexp::List(sexp));
+ return os;
+}
+MU_ENABLE_BITOPS(Sexp::FormattingOptions);
+
+} // namespace Mu
+
+#endif /* MU_SEXP_HH__ */
--- /dev/null
+/*
+** 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 <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <langinfo.h>
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "utils/mu-error.hh"
+
+
+using namespace Mu;
+
+char*
+Mu::test_mu_common_get_random_tmpdir()
+{
+ char* dir;
+ int res;
+
+ dir = g_strdup_printf("%s%cmu-test-%d%ctest-%x",
+ g_get_tmp_dir(),
+ G_DIR_SEPARATOR,
+ getuid(),
+ G_DIR_SEPARATOR,
+ (int)random() * getpid() * (int)time(NULL));
+
+ res = g_mkdir_with_parents(dir, 0700);
+ g_assert(res != -1);
+
+ return dir;
+}
+
+const char*
+Mu::set_tz(const char* tz)
+{
+ static const char* oldtz;
+
+ oldtz = getenv("TZ");
+ if (tz)
+ setenv("TZ", tz, 1);
+ else
+ unsetenv("TZ");
+
+ tzset();
+ return oldtz;
+}
+
+bool
+Mu::set_en_us_utf8_locale()
+{
+ setenv("LC_ALL", "en_US.UTF-8", 1);
+ setlocale(LC_ALL, "en_US.UTF-8");
+
+ if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) {
+ g_print("Note: Unit tests require the en_US.utf8 locale. "
+ "Ignoring test cases.\n");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+black_hole(void)
+{
+ return; /* do nothing */
+}
+
+void
+Mu::mu_test_init(int *argc, char ***argv)
+{
+ g_test_init(argc, argv, NULL);
+
+ if (!g_test_verbose())
+ g_log_set_handler(
+ NULL,
+ (GLogLevelFlags)(G_LOG_LEVEL_MASK |
+ G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+ (GLogFunc)black_hole, NULL);
+}
+
+
+
+void
+Mu::allow_warnings()
+{
+ g_test_log_set_fatal_handler(
+ [](const char*, GLogLevelFlags, const char*, gpointer) { return FALSE; },
+ {});
+}
+
+
+
+Mu::TempDir::TempDir(bool autodelete): autodelete_{autodelete}
+{
+ GError *err{};
+ gchar *tmpdir = g_dir_make_tmp("mu-tmp-XXXXXX", &err);
+ if (!tmpdir)
+ throw Mu::Error(Error::Code::File, &err,
+ "failed to create temporary directory");
+
+ path_ = tmpdir;
+ g_free(tmpdir);
+
+ g_debug("created '%s'", path_.c_str());
+}
+
+Mu::TempDir::~TempDir()
+{
+ if (::access(path_.c_str(), F_OK) != 0)
+ return; /* nothing to do */
+
+ if (!autodelete_) {
+ g_debug("_not_ deleting %s", path_.c_str());
+ return;
+ }
+
+ /* ugly */
+ GError *err{};
+ const auto cmd{format("/bin/rm -rf '%s'", path_.c_str())};
+ 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);
+ } else
+ g_debug("removed '%s'", path_.c_str());
+}
--- /dev/null
+/*
+** 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_TEST_UTILS_HH__
+#define MU_TEST_UTILS_HH__
+
+#include <string>
+
+namespace Mu {
+
+/**
+ * get a dir name for a random temporary directory to do tests
+ *
+ * @return a random dir name, g_free when it's no longer needed
+ */
+char* test_mu_common_get_random_tmpdir();
+
+/**
+ * mu wrapper for g_test_init
+ *
+ * @param argc
+ * @param argv
+ */
+void mu_test_init(int *argc, char ***argv);
+
+/**
+ * set the timezone
+ *
+ * @param tz timezone
+ *
+ * @return the old timezone
+ */
+const char* set_tz(const char* tz);
+
+/**
+ * switch the locale to en_US.utf8, return TRUE if it succeeds
+ *
+ * @return true if the switch succeeds, false otherwise
+ */
+bool set_en_us_utf8_locale();
+
+/**
+ * For unit tests, assert two std::string's are equal.
+ *
+ * @param s1 string1
+ * @param s2 string2
+ */
+#define assert_equal(s1__,s2__) do { \
+ std::string s1s__(s1__), s2s__(s2__); \
+ g_assert_cmpstr(s1s__.c_str(), ==, s2s__.c_str()); \
+ } while(0)
+
+
+#define assert_equal_seq(seq1__, seq2__) do { \
+ g_assert_cmpuint(seq1__.size(), ==, seq2__.size()); \
+ size_t n__{}; \
+ for (auto&& item__: seq1__) { \
+ g_assert_true(item__ == seq2__.at(n__)); \
+ ++n__; \
+ } \
+ } while(0)
+
+#define assert_equal_seq_str(seq1__, seq2__) do { \
+ g_assert_cmpuint(seq1__.size(), ==, seq2__.size()); \
+ size_t n__{}; \
+ for (auto&& item__: seq1__) { \
+ assert_equal(item__, seq2__.at(n__)); \
+ ++n__; \
+ } \
+ } while(0)
+
+/**
+ * For unit-tests, allow warnings in the current function.
+ *
+ */
+void allow_warnings();
+
+
+/**
+ * For unit-tests, a RAII tempdir.
+ *
+ */
+struct TempDir {
+ /**
+ * Construct a temporary directory
+ */
+ TempDir(bool autodelete=true);
+
+ /**
+ * DTOR; removes the temporary directory
+ *
+ *
+ * @return
+ */
+ ~TempDir();
+
+ /**
+ * Path to the temporary directory
+ *
+ * @return the path.
+ *
+ *
+ */
+ const std::string& path() {return path_; }
+private:
+ std::string path_;
+ const bool autodelete_;
+};
+
+} // namepace Mu
+
+
+#endif /* MU_TEST_UTILS_HH__ */
--- /dev/null
+/*
+** 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 <config.h>
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif /*_GNU_SOURCE*/
+
+#include "mu-util.h"
+#ifdef HAVE_WORDEXP_H
+#include <wordexp.h> /* for shell-style globbing */
+#endif /*HAVE_WORDEXP_H*/
+
+#include <stdlib.h>
+
+#include <string.h>
+#include <locale.h> /* for setlocale() */
+
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <glib-object.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+#include <errno.h>
+
+#include <langinfo.h>
+
+
+static char*
+do_wordexp (const char *path)
+{
+#ifdef HAVE_WORDEXP_H
+ wordexp_t wexp;
+ char *dir;
+
+ if (!path) {
+ /* g_debug ("%s: path is empty", __func__); */
+ return NULL;
+ }
+
+ if (wordexp (path, &wexp, 0) != 0) {
+ /* g_debug ("%s: expansion failed for %s", __func__, path); */
+ return NULL;
+ }
+
+ /* we just pick the first one */
+ dir = g_strdup (wexp.we_wordv[0]);
+
+ /* strangely, below seems to lead to a crash on MacOS (BSD);
+ so we have to allow for a tiny leak here on that
+ platform... maybe instead of __APPLE__ it should be
+ __BSD__?
+
+ Hmmm., cannot reproduce that crash anymore, so commenting
+ it out for now...
+ */
+/* #ifndef __APPLE__ */
+ wordfree (&wexp);
+/* #endif /\*__APPLE__*\/ */
+ return dir;
+
+# else /*!HAVE_WORDEXP_H*/
+/* E.g. OpenBSD does not have wordexp.h, so we ignore it */
+ return path ? g_strdup (path) : NULL;
+#endif /*HAVE_WORDEXP_H*/
+}
+
+
+/* note, the g_debugs are commented out because this function may be
+ * called before the log handler is installed. */
+char*
+mu_util_dir_expand (const char *path)
+{
+ char *dir;
+ char resolved[PATH_MAX + 1];
+
+ g_return_val_if_fail (path, NULL);
+
+ dir = do_wordexp (path);
+ if (!dir)
+ return NULL; /* error */
+
+ /* don't try realpath if the dir does not exist */
+ if (access (dir, F_OK) != 0)
+ return dir;
+
+ /* now resolve any symlinks, .. etc. */
+ if (realpath (dir, resolved) == NULL) {
+ /* g_debug ("%s: could not get realpath for '%s': %s", */
+ /* __func__, dir, g_strerror(errno)); */
+ g_free (dir);
+ return NULL;
+ } else
+ g_free (dir);
+
+ return g_strdup (resolved);
+}
+
+GQuark
+mu_util_error_quark (void)
+{
+ static GQuark error_domain = 0;
+
+ if (G_UNLIKELY(error_domain == 0))
+ error_domain = g_quark_from_static_string
+ ("mu-error-quark");
+
+ return error_domain;
+}
+
+gboolean
+mu_util_check_dir (const gchar* path, gboolean readable, gboolean writeable)
+{
+ int mode;
+ struct stat statbuf;
+
+ if (!path)
+ return FALSE;
+
+ mode = F_OK | (readable ? R_OK : 0) | (writeable ? W_OK : 0);
+
+ if (access (path, mode) != 0) {
+ /* g_debug ("Cannot access %s: %s", path, g_strerror (errno)); */
+ return FALSE;
+ }
+
+ if (stat (path, &statbuf) != 0) {
+ /* g_debug ("Cannot stat %s: %s", path, g_strerror (errno)); */
+ return FALSE;
+ }
+
+ return S_ISDIR(statbuf.st_mode) ? TRUE: FALSE;
+}
+
+
+gchar*
+mu_util_guess_maildir (void)
+{
+ const gchar *mdir1, *home;
+
+ /* first, try MAILDIR */
+ mdir1 = g_getenv ("MAILDIR");
+
+ if (mdir1 && mu_util_check_dir (mdir1, TRUE, FALSE))
+ return g_strdup (mdir1);
+
+ /* then, try <home>/Maildir */
+ home = g_get_home_dir();
+ if (home) {
+ char *mdir2;
+ mdir2 = g_strdup_printf ("%s%cMaildir",
+ home, G_DIR_SEPARATOR);
+ if (mu_util_check_dir (mdir2, TRUE, FALSE))
+ return mdir2;
+ g_free (mdir2);
+ }
+
+ /* nope; nothing found */
+ return NULL;
+}
+
+gboolean
+mu_util_create_dir_maybe (const gchar *path, mode_t mode, gboolean nowarn)
+{
+ struct stat statbuf;
+
+ g_return_val_if_fail (path, FALSE);
+
+ /* if it exists, it must be a readable dir */
+ if (stat (path, &statbuf) == 0) {
+ if ((!S_ISDIR(statbuf.st_mode)) ||
+ (access (path, W_OK|R_OK) != 0)) {
+ if (!nowarn)
+ g_warning ("not a read-writable"
+ "directory: %s", path);
+ return FALSE;
+ }
+ }
+
+ if (g_mkdir_with_parents (path, mode) != 0) {
+ if (!nowarn)
+ g_warning ("failed to create %s: %s",
+ path, g_strerror(errno));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+mu_util_supports (MuFeature feature)
+{
+ /* check for Guile support */
+#ifndef BUILD_GUILE
+ if (feature & MU_FEATURE_GUILE)
+ return FALSE;
+#endif /*BUILD_GUILE*/
+
+ /* check for Gnuplot */
+ if (feature & MU_FEATURE_GNUPLOT)
+ if (!mu_util_program_in_path ("gnuplot"))
+ return FALSE;
+
+ return TRUE;
+}
+
+
+gboolean
+mu_util_program_in_path (const char *prog)
+{
+ gchar *path;
+
+ g_return_val_if_fail (prog, FALSE);
+
+ path = g_find_program_in_path (prog);
+ g_free (path);
+
+ return (path != NULL) ? TRUE : FALSE;
+}
+
+
+/*
+ * Set the child to a group leader to avoid being killed when the
+ * parent group is killed.
+ */
+static void
+maybe_setsid (G_GNUC_UNUSED gpointer user_data)
+{
+#if HAVE_SETSID
+ setsid();
+#endif /*HAVE_SETSID*/
+}
+
+gboolean
+mu_util_play (const char *path, GError **err)
+{
+ GFile *gf;
+ gboolean rv, is_native;
+ const gchar *argv[3];
+ const char *prog;
+
+ g_return_val_if_fail (path, FALSE);
+
+ gf = g_file_new_for_path(path);
+ is_native = g_file_is_native(gf);
+ g_object_unref(gf);
+
+ if (!is_native) {
+ mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_EXECUTE,
+ "'%s' is not a native file", path);
+ return FALSE;
+ }
+
+ prog = g_getenv ("MU_PLAY_PROGRAM");
+ if (!prog) {
+#ifdef __APPLE__
+ prog = "open";
+#else
+ prog = "xdg-open";
+#endif /*!__APPLE__*/
+ }
+
+ if (!mu_util_program_in_path (prog)) {
+ mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_EXECUTE,
+ "cannot find '%s' in path", prog);
+ return FALSE;
+ }
+
+ argv[0] = prog;
+ argv[1] = path;
+ argv[2] = NULL;
+
+ err = NULL;
+ rv = g_spawn_async (NULL, (gchar**)&argv, NULL,
+ G_SPAWN_SEARCH_PATH, maybe_setsid,
+ NULL, NULL, err);
+ return rv;
+}
+
+
+unsigned char
+mu_util_get_dtype (const char *path, gboolean use_lstat)
+{
+ int res;
+ struct stat statbuf;
+
+ g_return_val_if_fail (path, DT_UNKNOWN);
+
+ if (use_lstat)
+ res = lstat (path, &statbuf);
+ else
+ res = stat (path, &statbuf);
+
+ if (res != 0) {
+ g_warning ("%sstat failed on %s: %s",
+ use_lstat ? "l" : "", path, g_strerror(errno));
+ return DT_UNKNOWN;
+ }
+
+ /* we only care about dirs, regular files and links */
+ if (S_ISREG (statbuf.st_mode))
+ return DT_REG;
+ else if (S_ISDIR (statbuf.st_mode))
+ return DT_DIR;
+ else if (S_ISLNK (statbuf.st_mode))
+ return DT_LNK;
+
+ return DT_UNKNOWN;
+}
+
+
+
+gboolean
+mu_util_locale_is_utf8 (void)
+{
+ const gchar *dummy;
+ static int is_utf8 = -1;
+
+ if (G_UNLIKELY(is_utf8 == -1))
+ is_utf8 = g_get_charset(&dummy) ? 1 : 0;
+
+ return is_utf8 ? TRUE : FALSE;
+}
+
+gboolean
+mu_util_fputs_encoded (const char *str, FILE *stream)
+{
+ int rv;
+ char *conv;
+
+ g_return_val_if_fail (stream, FALSE);
+
+ /* g_get_charset return TRUE when the locale is UTF8 */
+ if (mu_util_locale_is_utf8())
+ return fputs (str, stream) == EOF ? FALSE : TRUE;
+
+ /* charset is _not_ utf8, so we need to convert it */
+ conv = NULL;
+ if (g_utf8_validate (str, -1, NULL))
+ conv = g_locale_from_utf8 (str, -1, NULL, NULL, NULL);
+
+ /* conversion failed; this happens because is some cases GMime may gives
+ * us non-UTF-8 strings from e.g. wrongly encoded message-subjects; if
+ * so, we escape the string */
+ conv = conv ? conv : g_strescape (str, "\n\t");
+ rv = conv ? fputs (conv, stream) : EOF;
+ g_free (conv);
+
+ return (rv == EOF) ? FALSE : TRUE;
+}
+
+
+
+gboolean
+mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...)
+{
+ va_list ap;
+ char *msg;
+
+ /* don't bother with NULL errors, or errors already set */
+ if (!err || *err)
+ return FALSE;
+
+ msg = NULL;
+ va_start (ap, frm);
+ g_vasprintf (&msg, frm, ap);
+ va_end (ap);
+
+ g_set_error (err, MU_ERROR_DOMAIN, errcode, "%s", msg);
+
+ g_free (msg);
+
+ return FALSE;
+}
+
+
+
+__attribute__((format(printf, 2, 0))) static gboolean
+print_args (FILE *stream, const char *frm, va_list args)
+{
+ gchar *str;
+ gboolean rv;
+
+ str = g_strdup_vprintf (frm, args);
+
+ rv = mu_util_fputs_encoded (str, stream);
+
+ g_free (str);
+
+ return rv;
+}
+
+
+gboolean
+mu_util_print_encoded (const char *frm, ...)
+{
+ va_list args;
+ gboolean rv;
+
+ g_return_val_if_fail (frm, FALSE);
+
+ va_start (args, frm);
+ rv = print_args (stdout, frm, args);
+ va_end (args);
+
+ return rv;
+}
+
+char*
+mu_str_summarize (const char* str, size_t max_lines)
+{
+ char *summary;
+ size_t nl_seen;
+ unsigned i,j;
+ gboolean last_was_blank;
+
+ g_return_val_if_fail (str, NULL);
+ g_return_val_if_fail (max_lines > 0, NULL);
+
+ /* len for summary <= original len */
+ summary = g_new (gchar, strlen(str) + 1);
+
+ /* copy the string up to max_lines lines, replace CR/LF/tab with
+ * single space */
+ for (i = j = 0, nl_seen = 0, last_was_blank = TRUE;
+ nl_seen < max_lines && str[i] != '\0'; ++i) {
+
+ if (str[i] == '\n' || str[i] == '\r' ||
+ str[i] == '\t' || str[i] == ' ' ) {
+
+ if (str[i] == '\n')
+ ++nl_seen;
+
+ /* no double-blanks or blank at end of str */
+ if (!last_was_blank && str[i+1] != '\0')
+ summary[j++] = ' ';
+
+ last_was_blank = TRUE;
+ } else {
+
+ summary[j++] = str[i];
+ last_was_blank = FALSE;
+ }
+ }
+
+ summary[j] = '\0';
+ return summary;
+}
--- /dev/null
+/*
+** 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 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_UTIL_H__
+#define __MU_UTIL_H__
+
+#include <glib.h>
+#include <stdio.h>
+#include <dirent.h>
+#include <sys/stat.h> /* for mode_t */
+
+/* hopefully, this should get us a sane PATH_MAX */
+#include <limits.h>
+/* not all systems provide PATH_MAX in limits.h */
+#ifndef PATH_MAX
+#include <sys/param.h>
+#ifndef PATH_MAX
+#define PATH_MAX MAXPATHLEN
+#endif /*!PATH_MAX*/
+#endif /*PATH_MAX*/
+
+G_BEGIN_DECLS
+
+/**
+ * get the expanded path; ie. perform shell expansion on the path. the
+ * path does not have to exist
+ *
+ * @param path path to expand
+ *
+ * @return the expanded path as a newly allocated string, or NULL in
+ * case of error
+ */
+char* mu_util_dir_expand(const char* path) G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * guess the maildir; first try $MAILDIR; if it is unset or
+ * non-existent, try ~/Maildir if both fail, return NULL
+ *
+ * @return full path of the guessed Maildir, or NULL; must be freed (gfree)
+ */
+char* mu_util_guess_maildir (void)
+ G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * if path exists, check that's a read/writeable dir; otherwise try to
+ * create it (with perms 0700)
+ *
+ * @param path path to the dir
+ * @param mode to set for the dir (as per chmod(1))
+ * @param nowarn, if TRUE, don't write warnings (if any) to stderr
+ *
+ * @return TRUE if a read/writeable directory `path' exists after
+ * leaving this function, FALSE otherwise
+ */
+gboolean mu_util_create_dir_maybe (const gchar *path, mode_t mode,
+ gboolean nowarn) G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * check whether path is a directory, and optionally, if it's readable
+ * and/or writeable
+ *
+ * @param path dir path
+ * @param readable check for readability
+ * @param writeable check for writability
+ *
+ * @return TRUE if dir exist and has the specified properties
+ */
+gboolean mu_util_check_dir (const gchar* path, gboolean readable,
+ gboolean writeable)
+ G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * is the current locale utf-8 compatible?
+ *
+ * @return TRUE if it's utf8 compatible, FALSE otherwise
+ */
+gboolean mu_util_locale_is_utf8 (void) G_GNUC_CONST;
+
+/**
+ * get a 'summary' of the string, ie. the first /n/ lines of the
+ * strings, with all newlines removed, replaced by single spaces
+ *
+ * @param str the source string
+ * @param max_lines the maximum number of lines to include in the summary
+ *
+ * @return a newly allocated string with the summary. use g_free to free it.
+ */
+char* mu_str_summarize (const char* str, size_t max_lines)
+ G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * write a string (assumed to be in utf8-format) to a stream,
+ * converted to the current locale
+ *
+ * @param str a string
+ * @param stream a stream
+ *
+ * @return TRUE if printing worked, FALSE otherwise
+ */
+gboolean mu_util_fputs_encoded (const char *str, FILE *stream);
+
+/**
+ * print a formatted string (assumed to be in utf8-format) to stdout,
+ * converted to the current locale
+ *
+ * @param a standard printf() format string, followed by a parameter list
+ *
+ * @return TRUE if printing worked, FALSE otherwise
+ */
+gboolean mu_util_print_encoded (const char *frm, ...) G_GNUC_PRINTF(1,2);
+
+
+/**
+ * Try to 'play' (ie., open with it's associated program) a file. On MacOS, the
+ * the program 'open' is used for this; on other platforms 'xdg-open' to do the
+ * actual opening. In addition you can set it to another program by setting the
+ * MU_PLAY_PROGRAM environment variable
+ *
+ * This requires a 'native' file, see g_file_is_native()
+ *
+ * @param path full path of the file to open
+ * @param err receives error information, if any
+ *
+ * @return TRUE if it succeeded, FALSE otherwise
+ */
+gboolean mu_util_play (const char *path, GError **err);
+
+/**
+ * Check whether program prog exists in PATH
+ *
+ * @param prog a program (executable)
+ *
+ * @return TRUE if it exists and is executable, FALSE otherwise
+ */
+gboolean mu_util_program_in_path (const char *prog);
+
+
+enum _MuFeature {
+ MU_FEATURE_GUILE = 1 << 0, /* do we support Guile 2.0? */
+ MU_FEATURE_GNUPLOT = 1 << 1, /* do we have gnuplot installed? */
+};
+typedef enum _MuFeature MuFeature;
+
+/**
+ * Check whether mu supports some particular feature
+ *
+ * @param feature a feature (multiple features can be logical-or'd together)
+ *
+ * @return TRUE if the feature is supported, FALSE otherwise
+ */
+gboolean mu_util_supports (MuFeature feature);
+
+
+
+/**
+ * Get an error-query for mu, to be used in `g_set_error'. Recent
+ * version of Glib warn when using 0 for the error-domain in
+ * g_set_error.
+ *
+ *
+ * @return an error quark for mu
+ */
+GQuark mu_util_error_quark (void) G_GNUC_CONST;
+#define MU_ERROR_DOMAIN (mu_util_error_quark())
+
+
+/*
+ * for OSs with out support for direntry->d_type, like Solaris
+ */
+#ifndef DT_UNKNOWN
+enum {
+ DT_UNKNOWN = 0,
+#define DT_UNKNOWN DT_UNKNOWN
+ DT_FIFO = 1,
+#define DT_FIFO DT_FIFO
+ DT_CHR = 2,
+#define DT_CHR DT_CHR
+ DT_DIR = 4,
+#define DT_DIR DT_DIR
+ DT_BLK = 6,
+#define DT_BLK DT_BLK
+ DT_REG = 8,
+#define DT_REG DT_REG
+ DT_LNK = 10,
+#define DT_LNK DT_LNK
+ DT_SOCK = 12,
+#define DT_SOCK DT_SOCK
+ DT_WHT = 14
+#define DT_WHT DT_WHT
+};
+#endif /*DT_UNKNOWN*/
+
+
+/**
+ * get the d_type (as in direntry->d_type) for the file at path, using either
+ * stat(3) or lstat(3)
+ *
+ * @param path full path
+ * @param use_lstat whether to use lstat (otherwise use stat)
+ *
+ * @return DT_REG, DT_DIR, DT_LNK, or DT_UNKNOWN (other values are not supported
+ * currently )
+ */
+unsigned char mu_util_get_dtype (const char *path, gboolean use_lstat);
+
+
+/**
+ * we need this when using Xapian::Document* from C
+ *
+ */
+typedef gpointer XapianDocument;
+
+/**
+ * we need this when using Xapian::Enquire* from C
+ *
+ */
+typedef gpointer XapianEnquire;
+
+
+/* print a warning for a GError, and free it */
+#define MU_HANDLE_G_ERROR(GE) \
+ do { \
+ if (!(GE)) \
+ g_warning ("%s:%u: an error occurred in %s", \
+ __FILE__, __LINE__, __func__); \
+ else { \
+ g_warning ("error %u: %s", (GE)->code, (GE)->message); \
+ g_error_free ((GE)); \
+ } \
+ } while (0)
+
+
+#define MU_G_ERROR_CODE(GE) ((GE)&&(*(GE))?(MuError)(*(GE))->code:MU_ERROR)
+
+
+enum _MuError {
+ /* no error at all! */
+ MU_OK = 0,
+
+ /* generic error */
+ MU_ERROR = 1,
+ MU_ERROR_IN_PARAMETERS = 2,
+ MU_ERROR_INTERNAL = 3,
+ MU_ERROR_NO_MATCHES = 4,
+
+ /* not really an error; for callbacks */
+ MU_IGNORE = 5,
+
+ MU_ERROR_SCRIPT_NOT_FOUND = 8,
+
+ /* general xapian related error */
+ MU_ERROR_XAPIAN = 11,
+
+ /* (parsing) error in the query */
+ MU_ERROR_XAPIAN_QUERY = 13,
+
+ /* missing data for a document */
+ MU_ERROR_XAPIAN_MISSING_DATA = 17,
+ /* can't get write lock */
+ MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK = 19,
+ /* could not write */
+ MU_ERROR_XAPIAN_STORE_FAILED = 21,
+ /* could not remove */
+ MU_ERROR_XAPIAN_REMOVE_FAILED = 22,
+ /* database was modified; reload */
+ MU_ERROR_XAPIAN_MODIFIED = 23,
+ /* database was modified; reload */
+ MU_ERROR_XAPIAN_NEEDS_REINDEX = 24,
+ /* database schema version doesn't match */
+ MU_ERROR_XAPIAN_SCHEMA_MISMATCH = 25,
+ /* failed to open the database */
+ MU_ERROR_XAPIAN_CANNOT_OPEN = 26,
+
+ /* GMime related errors */
+
+ /* gmime parsing related error */
+ MU_ERROR_GMIME = 30,
+
+ /* contacts related errors */
+ MU_ERROR_CONTACTS = 50,
+ MU_ERROR_CONTACTS_CANNOT_RETRIEVE = 51,
+
+ /* crypto related errors */
+ MU_ERROR_CRYPTO = 60,
+
+
+ /* File errors */
+ /* generic file-related error */
+ MU_ERROR_FILE = 70,
+ MU_ERROR_FILE_INVALID_NAME = 71,
+ MU_ERROR_FILE_CANNOT_LINK = 72,
+ MU_ERROR_FILE_CANNOT_OPEN = 73,
+ MU_ERROR_FILE_CANNOT_READ = 74,
+ MU_ERROR_FILE_CANNOT_EXECUTE = 75,
+ MU_ERROR_FILE_CANNOT_CREATE = 76,
+ MU_ERROR_FILE_CANNOT_MKDIR = 77,
+ MU_ERROR_FILE_STAT_FAILED = 78,
+ MU_ERROR_FILE_READDIR_FAILED = 79,
+ MU_ERROR_FILE_INVALID_SOURCE = 80,
+ MU_ERROR_FILE_TARGET_EQUALS_SOURCE = 81,
+ MU_ERROR_FILE_CANNOT_WRITE = 82,
+ MU_ERROR_FILE_CANNOT_UNLINK = 83,
+
+ /* not really an error, used in callbacks */
+ MU_STOP = 99
+};
+typedef enum _MuError MuError;
+
+
+/**
+ * set an error if it's not already set, and return FALSE
+ *
+ * @param err errptr, or NULL
+ * @param errcode error code
+ * @param frm printf-style format, followed by parameters
+ *
+ * @return FALSE
+ */
+gboolean mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...)
+ G_GNUC_PRINTF(3,4);
+
+#define MU_COLOR_RED "\x1b[31m"
+#define MU_COLOR_GREEN "\x1b[32m"
+#define MU_COLOR_YELLOW "\x1b[33m"
+#define MU_COLOR_BLUE "\x1b[34m"
+#define MU_COLOR_MAGENTA "\x1b[35m"
+#define MU_COLOR_CYAN "\x1b[36m"
+#define MU_COLOR_DEFAULT "\x1b[0m"
+
+G_END_DECLS
+
+#endif /*__MU_UTIL_H__*/
--- /dev/null
+/*
+** 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_UTILS_FORMAT_HH__
+#define MU_UTILS_FORMAT_HH__
+
+#include <string>
+#include <cstdarg>
+
+namespace Mu {
+
+/**
+ * Quote & escape a string for " and \
+ *
+ * @param str a string
+ *
+ * @return quoted string
+ */
+std::string quote(const std::string& str);
+
+/**
+ * Format a string, printf style
+ *
+ * @param frm format string
+ * @param ... parameters
+ *
+ * @return a formatted string
+ */
+std::string format(const char* frm, ...) __attribute__((format(printf, 1, 2)));
+
+/**
+ * Format a string, printf style
+ *
+ * @param frm format string
+ * @param ... parameters
+ *
+ * @return a formatted string
+ */
+std::string vformat(const char* frm, va_list args) __attribute__((format(printf, 1, 0)));
+
+
+} // namepace Mu
+
+
+#endif /* MU_UTILS_FORMAT_HH__ */
--- /dev/null
+/*
+** 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.
+*/
+
+#ifndef _XOPEN_SOURCE
+#define _XOPEN_SOURCE
+#include <stdexcept>
+#endif /*_XOPEN_SOURCE*/
+
+#include <array>
+
+#include <time.h>
+
+#define GNU_SOURCE
+#include <stdio.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+#include <numeric>
+#include <functional>
+#include <cinttypes>
+#include <charconv>
+#include <limits>
+#include <regex>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "mu-utils.hh"
+#include "mu-utils-format.hh"
+#include "mu-util.h"
+#include "mu-error.hh"
+#include "mu-option.hh"
+
+using namespace Mu;
+
+namespace {
+
+static gunichar
+unichar_tolower(gunichar uc)
+{
+ if (!g_unichar_isalpha(uc))
+ return uc;
+
+ if (g_unichar_get_script(uc) != G_UNICODE_SCRIPT_LATIN)
+ return g_unichar_tolower(uc);
+
+ switch (uc) {
+ case 0x00e6:
+ case 0x00c6: return 'e'; /* æ */
+ case 0x00f8: return 'o'; /* ø */
+ case 0x0110:
+ case 0x0111:
+ return 'd'; /* đ */
+ /* todo: many more */
+ default: return g_unichar_tolower(uc);
+ }
+}
+
+/**
+ * gx_utf8_flatten:
+ * @str: a UTF-8 string
+ * @len: the length of @str, or -1 if it is %NULL-terminated
+ *
+ * Flatten some UTF-8 string; that is, downcase it and remove any diacritics.
+ *
+ * Returns: (transfer full): a flattened string, free with g_free().
+ */
+static char*
+gx_utf8_flatten(const gchar* str, gssize len)
+{
+ GString* gstr;
+ char * norm, *cur;
+
+ g_return_val_if_fail(str, NULL);
+
+ norm = g_utf8_normalize(str, len, G_NORMALIZE_ALL);
+ if (!norm)
+ return NULL;
+
+ gstr = g_string_sized_new(strlen(norm));
+
+ for (cur = norm; cur && *cur; cur = g_utf8_next_char(cur)) {
+ gunichar uc;
+
+ uc = g_utf8_get_char(cur);
+ if (g_unichar_combining_class(uc) != 0)
+ continue;
+
+ g_string_append_unichar(gstr, unichar_tolower(uc));
+ }
+
+ g_free(norm);
+
+ return g_string_free(gstr, FALSE);
+}
+
+} // namespace
+
+std::string // gx_utf8_flatten
+Mu::utf8_flatten(const char* str)
+{
+ if (!str)
+ return {};
+
+ // the pure-ascii case
+ if (g_str_is_ascii(str)) {
+ auto l = g_ascii_strdown(str, -1);
+ std::string s{l};
+ g_free(l);
+ return s;
+ }
+
+ // seems we need the big guns
+ char* flat = gx_utf8_flatten(str, -1);
+ if (!flat)
+ return {};
+
+ std::string s{flat};
+ g_free(flat);
+
+ return s;
+}
+
+
+/* turn \0-terminated buf into ascii (which is a utf8 subset); convert
+ * any non-ascii into '.'
+ */
+static char*
+asciify_in_place (char *buf)
+{
+ char *c;
+
+ g_return_val_if_fail (buf, NULL);
+
+ for (c = buf; c && *c; ++c) {
+ if ((!isprint(*c) && !isspace (*c)) || !isascii(*c))
+ *c = '.';
+ }
+
+ return buf;
+}
+
+static char*
+utf8ify (const char *buf)
+{
+ char *utf8;
+
+ g_return_val_if_fail (buf, NULL);
+
+ utf8 = g_strdup (buf);
+
+ if (!g_utf8_validate (buf, -1, NULL))
+ asciify_in_place (utf8);
+
+ return utf8;
+}
+
+
+std::string
+Mu::utf8_clean(const std::string& dirty)
+{
+ g_autoptr(GString) gstr = g_string_sized_new(dirty.length());
+ g_autofree char *cstr = utf8ify(dirty.c_str());
+
+ for (auto cur = cstr; cur && *cur; cur = g_utf8_next_char(cur)) {
+ const gunichar uc = g_utf8_get_char(cur);
+ if (g_unichar_iscntrl(uc))
+ g_string_append_c(gstr, ' ');
+ else
+ g_string_append_unichar(gstr, uc);
+ }
+
+ return std::string{g_strstrip(gstr->str)};
+}
+
+std::string
+Mu::remove_ctrl(const std::string& str)
+{
+ char prev{'\0'};
+ std::string result;
+ result.reserve(str.length());
+
+ for (auto&& c : str) {
+ if (::iscntrl(c) || c == ' ') {
+ if (prev != ' ')
+ result += prev = ' ';
+ } else
+ result += prev = c;
+ }
+
+ return result;
+}
+
+std::vector<std::string>
+Mu::split(const std::string& str, const std::string& sepa)
+{
+ std::vector<std::string> vec;
+ size_t b = 0, e = 0;
+
+ /* special cases */
+ if (str.empty())
+ return vec;
+ else if (sepa.empty()) {
+ for (auto&& c: str)
+ vec.emplace_back(1, c);
+ return vec;
+ }
+
+ while (true) {
+ if (e = str.find(sepa, b); e != std::string::npos) {
+ vec.emplace_back(str.substr(b, e - b));
+ b = e + sepa.length();
+ } else {
+ vec.emplace_back(str.substr(b));
+ break;
+ }
+ }
+
+ return vec;
+}
+
+std::vector<std::string>
+Mu::split(const std::string& str, char sepa)
+{
+ std::vector<std::string> vec;
+ size_t b = 0, e = 0;
+
+ /* special case */
+ if (str.empty())
+ return vec;
+
+ while (true) {
+ if (e = str.find(sepa, b); e != std::string::npos) {
+ vec.emplace_back(str.substr(b, e - b));
+ b = e + sizeof(sepa);
+ } else {
+ vec.emplace_back(str.substr(b));
+ break;
+ }
+ }
+
+ return vec;
+}
+
+std::vector<std::string>
+Mu::split(const std::string& str, const std::regex& sepa_rx)
+{
+ std::sregex_token_iterator it(str.begin(), str.end(), sepa_rx, -1);
+ std::sregex_token_iterator end;
+
+ return {it, end};
+}
+
+std::string
+Mu::join(const std::vector<std::string>& svec, const std::string& sepa)
+{
+ if (svec.empty())
+ return {};
+
+
+ /* calculate the overall size beforehand, to avoid re-allocations. */
+ size_t value_len =
+ std::accumulate(svec.cbegin(), svec.cend(), 0,
+ [](size_t size, const std::string& s) {
+ return size + s.size();
+ }) + (svec.size() - 1) * sepa.length();
+
+ std::string value;
+ value.reserve(value_len);
+
+ std::accumulate(svec.cbegin(), svec.cend(), std::ref(value),
+ [&](std::string& s1, const std::string& s2)->std::string& {
+ if (s1.empty())
+ s1 = s2;
+ else {
+ s1.append(sepa);
+ s1.append(s2);
+ }
+ return s1;
+ });
+
+ return value;
+}
+
+std::string
+Mu::quote(const std::string& str)
+{
+ std::string res{"\""};
+
+ for (auto&& k : str) {
+ switch (k) {
+ case '"': res += "\\\""; break;
+ case '\\': res += "\\\\"; break;
+ default: res += k;
+ }
+ }
+
+ return res + "\"";
+}
+
+std::string
+Mu::format(const char* frm, ...)
+{
+ va_list args;
+
+ va_start(args, frm);
+ auto str = vformat(frm, args);
+ va_end(args);
+
+ return str;
+}
+
+std::string
+Mu::vformat(const char* frm, va_list args)
+{
+ char* s{};
+ const auto res = g_vasprintf(&s, frm, args);
+ if (res == -1) {
+ std::cerr << "string format failed" << std::endl;
+ return {};
+ }
+
+ std::string str{s};
+ g_free(s);
+
+ return str;
+}
+
+std::string
+Mu::time_to_string(const char *frm, time_t t, bool utc)
+{
+ g_return_val_if_fail(frm, "");
+
+ GDateTime* dt = std::invoke([&] {
+ if (utc)
+ return g_date_time_new_from_unix_utc(t);
+ else
+ return g_date_time_new_from_unix_local(t);
+ });
+
+ if (!dt) {
+ g_warning("time_t out of range: <%" G_GUINT64_FORMAT ">",
+ static_cast<guint64>(t));
+ return {};
+ }
+
+ frm = frm ? frm : "%c";
+ auto datestr{to_string_opt_gchar(g_date_time_format(dt, frm))};
+ g_date_time_unref(dt);
+ if (!datestr)
+ g_warning("failed to format time with format '%s'", frm);
+
+ return datestr.value_or("");
+}
+
+static Option<int64_t>
+delta_ymwdhMs(const std::string& expr)
+{
+ char* endptr;
+ auto num = strtol(expr.c_str(), &endptr, 10);
+ if (num <= 0 || num > 9999 || !endptr || !*endptr)
+ return Nothing;
+
+ int years, months, weeks, days, hours, minutes, seconds;
+ years = months = weeks = days = hours = minutes = seconds = 0;
+
+ switch (endptr[0]) {
+ case 's': seconds = num; break;
+ case 'M': minutes = num; break;
+ case 'h': hours = num; break;
+ case 'd': days = num; break;
+ case 'w': weeks = num; break;
+ case 'm': months = num; break;
+ case 'y': years = num; break;
+ default:
+ return Nothing;
+ }
+
+ GDateTime *then, *now = g_date_time_new_now_local();
+ if (weeks != 0)
+ then = g_date_time_add_weeks(now, -weeks);
+ else
+ then =
+ g_date_time_add_full(now, -years, -months, -days, -hours, -minutes, -seconds);
+
+ auto t = std::max<int64_t>(0, g_date_time_to_unix(then));
+
+ g_date_time_unref(then);
+ g_date_time_unref(now);
+
+ return t;
+}
+
+static Option<int64_t>
+special_date_time(const std::string& d, bool is_first)
+{
+ if (d == "now")
+ return ::time({});
+
+ if (d == "today") {
+ GDateTime *dt, *midnight;
+ dt = g_date_time_new_now_local();
+
+ if (!is_first) {
+ GDateTime* tmp = dt;
+ dt = g_date_time_add_days(dt, 1);
+ g_date_time_unref(tmp);
+ }
+
+ midnight = g_date_time_add_full(dt,
+ 0,
+ 0,
+ 0,
+ -g_date_time_get_hour(dt),
+ -g_date_time_get_minute(dt),
+ -g_date_time_get_second(dt));
+ time_t t = MAX(0, (gint64)g_date_time_to_unix(midnight));
+ g_date_time_unref(dt);
+ g_date_time_unref(midnight);
+
+ return t;
+ }
+
+ return Nothing;
+}
+
+// if a date has a month day greater than the number of days in that month,
+// change it to a valid date point to the last second in that month
+static void
+fixup_month(struct tm* tbuf)
+{
+ decltype(tbuf->tm_mday) max_days;
+ const auto month = tbuf->tm_mon + 1;
+ const auto year = tbuf->tm_year + 1900;
+
+ switch (month) {
+ case 2:
+ if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))
+ max_days = 29;
+ else
+ max_days = 28;
+ break;
+ case 4:
+ case 6:
+ case 9:
+ case 11:
+ max_days = 30;
+ break;
+ default:
+ max_days = 31;
+ break;
+ }
+
+ if (tbuf->tm_mday > max_days) {
+ tbuf->tm_mday = max_days;
+ tbuf->tm_hour = 23;
+ tbuf->tm_min = 59;
+ tbuf->tm_sec = 59;
+ }
+ }
+
+
+Option<int64_t>
+Mu::parse_date_time(const std::string& dstr, bool is_first)
+{
+ struct tm tbuf{};
+ GDateTime *dtime{};
+ int64_t t;
+
+ /* one-sided dates */
+ if (dstr.empty())
+ return is_first ? 0 : G_MAXINT64;
+ else if (dstr == "today" || dstr == "now")
+ return special_date_time(dstr, is_first);
+ else if (dstr.find_first_of("ymdwhMs") != std::string::npos)
+ return delta_ymwdhMs(dstr);
+
+ constexpr char UserDateMin[] = "19700101000000";
+ constexpr char UserDateMax[] = "29991231235959";
+
+ std::string date(is_first ? UserDateMin : UserDateMax);
+ std::copy_if(dstr.begin(), dstr.end(), date.begin(), [](auto c) { return isdigit(c); });
+
+ if (!::strptime(date.c_str(), "%Y%m%d%H%M%S", &tbuf) &&
+ !::strptime(date.c_str(), "%Y%m%d%H%M", &tbuf) &&
+ !::strptime(date.c_str(), "%Y%m%d%H", &tbuf) &&
+ !::strptime(date.c_str(), "%Y%m%d", &tbuf) &&
+ !::strptime(date.c_str(), "%Y%m", &tbuf) &&
+ !::strptime(date.c_str(), "%Y", &tbuf))
+ return Nothing;
+
+ fixup_month(&tbuf);
+ dtime = g_date_time_new_local(tbuf.tm_year + 1900,
+ tbuf.tm_mon + 1,
+ tbuf.tm_mday,
+ tbuf.tm_hour,
+ tbuf.tm_min,
+ tbuf.tm_sec);
+ t = g_date_time_to_unix(dtime);
+ g_date_time_unref(dtime);
+
+ return std::max<int64_t>(t, 0);
+}
+
+
+Option<int64_t>
+Mu::parse_size(const std::string& val, bool is_first)
+{
+ int64_t size{-1};
+ std::string str;
+ GRegex* rx;
+ GMatchInfo* minfo;
+
+ /* one-sided ranges */
+ if (val.empty())
+ return is_first ? 0 : std::numeric_limits<int64_t>::max();
+
+ rx = g_regex_new("^(\\d+)(b|k|kb|m|mb|g|gb)?$",
+ G_REGEX_CASELESS, (GRegexMatchFlags)0, NULL);
+ minfo = NULL;
+ if (g_regex_match(rx, val.c_str(), (GRegexMatchFlags)0, &minfo)) {
+
+ char* s;
+ s = g_match_info_fetch(minfo, 1);
+ size = atoll(s);
+ g_free(s);
+
+ s = g_match_info_fetch(minfo, 2);
+ switch (s ? g_ascii_tolower(s[0]) : 0) {
+ case 'k': size *= 1024; break;
+ case 'm': size *= (1024 * 1024); break;
+ case 'g': size *= (1024 * 1024 * 1024); break;
+ default: break;
+ }
+
+ g_free(s);
+ }
+
+ g_regex_unref(rx);
+ g_match_info_unref(minfo);
+
+ if (size < 0)
+ return Nothing;
+ else
+ return size;
+
+}
+
+std::string
+Mu::to_lexnum(int64_t val)
+{
+ char buf[18]; /* 1 byte prefix + hex + \0 */
+ buf[0] = 'f' + ::snprintf(buf + 1, sizeof(buf) - 1, "%" PRIx64, val);
+ return buf;
+}
+
+int64_t
+Mu::from_lexnum(const std::string& str)
+{
+ int64_t val{};
+ std::from_chars(str.c_str() + 1, str.c_str() + str.size(), val, 16);
+
+ return val;
+}
+
+
+std::string
+Mu::canonicalize_filename(const std::string& path, const std::string& relative_to)
+{
+ auto str{to_string_opt_gchar(
+ g_canonicalize_filename(
+ path.c_str(),
+ relative_to.empty() ? nullptr : relative_to.c_str())).value()};
+
+ // remove trailing '/'... is this needed?
+ if (str[str.length()-1] == G_DIR_SEPARATOR)
+ str.erase(str.length() - 1);
+
+ return str;
+}
+
+
+bool
+Mu::locale_workaround() try
+{
+ // quite horrible... but some systems break otherwise with
+ // https://github.com/djcb/mu/issues/2252
+
+ try {
+ std::locale::global(std::locale(""));
+ } catch (const std::runtime_error& re) {
+ g_setenv("LC_ALL", "C", 1);
+ std::locale::global(std::locale(""));
+ }
+
+ return true;
+
+} catch (...) {
+ return false;
+}
+
+bool
+Mu::timezone_available(const std::string& tz)
+{
+ const auto old_tz = g_getenv("TZ");
+
+ g_setenv("TZ", tz.c_str(), TRUE);
+
+ auto tzone = g_time_zone_new_local ();
+ bool have_tz = g_strcmp0(g_time_zone_get_identifier(tzone), tz.c_str()) == 0;
+ g_time_zone_unref (tzone);
+
+ if (old_tz)
+ g_setenv("TZ", old_tz, TRUE);
+ else
+ g_unsetenv("TZ");
+
+ return have_tz;
+}
--- /dev/null
+/*
+** Copyright (C) 2020-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_UTILS_HH__
+#define __MU_UTILS_HH__
+
+#include <string>
+#include <string_view>
+#include <sstream>
+#include <vector>
+#include <chrono>
+#include <memory>
+#include <cstdarg>
+#include <glib.h>
+#include <ostream>
+#include <iostream>
+#include <type_traits>
+#include <algorithm>
+#include <numeric>
+#include <regex>
+
+#include "mu-utils-format.hh"
+#include "mu-option.hh"
+
+namespace Mu {
+
+using StringVec = std::vector<std::string>;
+
+/**
+ * Flatten a string -- downcase and fold diacritics etc.
+ *
+ * @param str a string
+ *
+ * @return a flattened string
+ */
+std::string utf8_flatten(const char* str);
+inline std::string
+utf8_flatten(const std::string& s)
+{
+ return utf8_flatten(s.c_str());
+}
+
+/**
+ * Replace all control characters with spaces, and remove leading and trailing space.
+ *
+ * @param dirty an unclean string
+ *
+ * @return a cleaned-up string.
+ */
+std::string utf8_clean(const std::string& dirty);
+
+/**
+ * Remove ctrl characters, replacing them with ' '; subsequent
+ * ctrl characters are replaced by a single ' '
+ *
+ * @param str a string
+ *
+ * @return the string without control characters
+ */
+std::string remove_ctrl(const std::string& str);
+
+/**
+ * Split a string in parts. As a special case, splitting an empty string
+ * yields an empty vector (not a vector with a single empty element)
+ *
+ * @param str a string
+ * @param sepa the separator
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split(const std::string& str, const std::string& sepa);
+
+/**
+ * Split a string in parts. As a special case, splitting an empty string
+ * yields an empty vector (not a vector with a single empty element)
+ *
+ * @param str a string
+ * @param sepa the separator
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split(const std::string& str, char sepa);
+
+/**
+ * Split a string in parts
+ *
+ * @param str a string
+ * @param sepa the separator regex
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split(const std::string& str, const std::regex& sepa_rx);
+
+/**
+ * Join the strings in svec into a string, separated by sepa
+ *
+ * @param svec a string vector
+ * @param sepa separator
+ *
+ * @return string
+ */
+std::string join(const std::vector<std::string>& svec, const std::string& sepa);
+static inline std::string join(const std::vector<std::string>& svec, char sepa) {
+ return join(svec, std::string(1, sepa));
+}
+
+/**
+ * Parse a date string to the corresponding time_t
+ * *
+ * @param date the date expressed a YYYYMMDDHHMMSS or any n... of the first
+ * characters, using the local timezone.
+ * @param first whether to fill out incomplete dates to the start or the end;
+ * ie. either 1972 -> 197201010000 or 1972 -> 197212312359
+ *
+ * @return the corresponding time_t or Nothing if parsing failed.
+ */
+Option<int64_t> parse_date_time(const std::string& date, bool first);
+
+/**
+ * 64-bit incarnation of time_t expressed as a 10-digit string. Uses 64-bit for the time-value,
+ * regardless of the size of time_t.
+ *
+ * @param t some time value
+ *
+ * @return
+ */
+std::string date_to_time_t_string(int64_t t);
+
+/**
+ * Get a string for a given time_t and format
+ * memory that must be freed after use.
+ *
+ * @param frm the format of the string (in strftime(3) format)
+ * @param t the time as time_t
+ * @param utc whether to display as UTC(if true) or local time
+ *
+ * @return a string representation of the time in UTF8-format, or empty in case
+ * of error.
+ */
+std::string time_to_string(const char *frm, time_t t, bool utc = false) G_GNUC_CONST;
+
+
+/**
+ * Hack to avoid locale crashes
+ *
+ * @return true if setting locale worked; false otherwise
+ */
+bool locale_workaround();
+
+
+/**
+ * Is the given timezone available? For tests
+ *
+ * @param tz a timezone, such as Europe/Helsinki
+ *
+ * @return true or false
+ */
+bool timezone_available(const std::string& tz);
+
+
+
+// https://stackoverflow.com/questions/19053351/how-do-i-use-a-custom-deleter-with-a-stdunique-ptr-member
+template <auto fn>
+struct deleter_from_fn {
+ template <typename T>
+ constexpr void operator()(T* arg) const {
+ fn(arg);
+ }
+};
+template <typename T, auto fn>
+using deletable_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
+
+
+
+using Clock = std::chrono::steady_clock;
+using Duration = Clock::duration;
+
+template <typename Unit>
+constexpr int64_t
+to_unit(Duration d)
+{
+ using namespace std::chrono;
+ return duration_cast<Unit>(d).count();
+}
+
+constexpr int64_t
+to_s(Duration d)
+{
+ return to_unit<std::chrono::seconds>(d);
+}
+constexpr int64_t
+to_ms(Duration d)
+{
+ return to_unit<std::chrono::milliseconds>(d);
+}
+constexpr int64_t
+to_us(Duration d)
+{
+ return to_unit<std::chrono::microseconds>(d);
+}
+
+struct StopWatch {
+ using Clock = std::chrono::steady_clock;
+ StopWatch(const std::string name) : start_{Clock::now()}, name_{name} {}
+ ~StopWatch()
+ {
+ const auto us{static_cast<double>(to_us(Clock::now() - start_))};
+ if (us > 2000000)
+ g_debug("%s: finished after %0.1f s", name_.c_str(), us / 1000000);
+ else if (us > 2000)
+ g_debug("%s: finished after %0.1f ms", name_.c_str(), us / 1000);
+ else
+ g_debug("%s: finished after %g us", name_.c_str(), us);
+ }
+
+private:
+ Clock::time_point start_;
+ std::string name_;
+};
+
+/**
+ * See g_canonicalize_filename
+ *
+ * @param filename
+ * @param relative_to
+ *
+ * @return
+ */
+std::string canonicalize_filename(const std::string& path, const std::string& relative_to);
+
+/**
+ * Convert a size string to a size in bytes
+ *
+ * @param sizestr the size string
+ * @param first
+ *
+ * @return the size or Nothing if parsing failed
+ */
+Option<int64_t> parse_size(const std::string& sizestr, bool first);
+
+/**
+ * Convert a size into a size in bytes string
+ *
+ * @param size the size
+ * @param first
+ *
+ * @return the size expressed as a string with the decimal number of bytes
+ */
+std::string size_to_string(int64_t size);
+
+/**
+ * Convert any ostreamable<< value to a string
+ *
+ * @param t the value
+ *
+ * @return a std::string
+ */
+template <typename T>
+static inline std::string
+to_string(const T& val)
+{
+ std::stringstream sstr;
+ sstr << val;
+
+ return sstr.str();
+}
+
+/**
+ * Consume a gchar and return a std::string
+ *
+ * @param str a gchar* (consumed/freed)
+ *
+ * @return a std::string, empty if gchar was {}
+ */
+static inline std::string
+to_string_gchar(gchar*&& str)
+{
+ std::string s(str?str:"");
+ g_free(str);
+ return s;
+}
+
+
+/*
+ * Lexicals Number are lexicographically sortable string representations
+ * of numbers. Start with 'g' + length of number in hex, followed by
+ * the ascii for the hex represntation. So,
+ *
+ * 0 -> 'g0'
+ * 1 -> 'g1'
+ * 10 -> 'ga'
+ * 16 -> 'h10'
+ *
+ * etc.
+ */
+std::string to_lexnum(int64_t val);
+int64_t from_lexnum(const std::string& str);
+
+/**
+ * Like std::find_if, but using sequence instead of a range.
+ *
+ * @param seq some std::find_if compatible sequence
+ * @param pred a predicate
+ *
+ * @return an iterator
+ */
+template<typename Sequence, typename UnaryPredicate>
+typename Sequence::const_iterator seq_find_if(const Sequence& seq, UnaryPredicate pred) {
+ return std::find_if(seq.cbegin(), seq.cend(), pred);
+}
+
+/**
+ * Is at least pred(element) true for at least one element of sequence
+ *
+ * @param seq sequence
+ * @param pred a predicate
+ *
+ * @return true or false
+ */
+template<typename Sequence, typename UnaryPredicate>
+bool seq_some(const Sequence& seq, UnaryPredicate pred) {
+ return seq_find_if(seq, pred) != seq.cend();
+}
+
+/**
+ * Create a sequence that has all element of seq for which pred is true
+ *
+ * @param seq sequence
+ * @param pred false
+ *
+ * @return sequence
+ */
+template<typename Sequence, typename UnaryPredicate>
+Sequence seq_filter(const Sequence& seq, UnaryPredicate pred) {
+ Sequence res;
+ std::copy_if(seq.begin(), seq.end(), std::back_inserter(res), pred);
+ return res;
+}
+
+/**
+ * Create a sequence that has all element of seq for which pred is false
+ *
+ * @param seq sequence
+ * @param pred false
+ *
+ * @return sequence
+ */
+template<typename Sequence, typename UnaryPredicate>
+Sequence seq_remove(const Sequence& seq, UnaryPredicate pred) {
+ Sequence res;
+ std::remove_copy_if(seq.begin(), seq.end(), std::back_inserter(res), pred);
+ return res;
+}
+
+template<typename Sequence, typename Compare>
+void seq_sort(Sequence& seq, Compare cmp) { std::sort(seq.begin(), seq.end(), cmp); }
+
+
+/**
+ * Like std::accumulate, but using a sequence instead of a range.
+ *
+ * @param seq some std::accumulate compatible sequence
+ * @param init the initial value
+ * @param op binary operation to calculate the next element
+ *
+ * @return the result value.
+ */
+template<typename Sequence, typename ResultType, typename BinaryOp>
+ResultType seq_fold(const Sequence& seq, ResultType init, BinaryOp op) {
+ return std::accumulate(seq.cbegin(), seq.cend(), init, op);
+}
+
+template<typename Sequence, typename UnaryOp>
+void seq_for_each(const Sequence& seq, UnaryOp op) {
+ std::for_each(seq.cbegin(), seq.cend(), op);
+}
+
+
+/**
+ * Convert string view in something printable with %*s
+ */
+#define STR_V(sv__) static_cast<int>((sv__).size()), (sv__).data()
+
+struct MaybeAnsi {
+ explicit MaybeAnsi(bool use_color) : color_{use_color} {}
+
+ enum struct Color {
+ Black = 30,
+ Red = 31,
+ Green = 32,
+ Yellow = 33,
+ Blue = 34,
+ Magenta = 35,
+ Cyan = 36,
+ White = 37,
+
+ BrightBlack = 90,
+ BrightRed = 91,
+ BrightGreen = 92,
+ BrightYellow = 93,
+ BrightBlue = 94,
+ BrightMagenta = 95,
+ BrightCyan = 96,
+ BrightWhite = 97,
+ };
+
+ std::string fg(Color c) const { return ansi(c, true); }
+ std::string bg(Color c) const { return ansi(c, false); }
+
+ std::string reset() const { return color_ ? "\x1b[0m" : ""; }
+
+private:
+ std::string ansi(Color c, bool fg = true) const
+ {
+ return color_ ? format("\x1b[%dm", static_cast<int>(c) + (fg ? 0 : 10)) : "";
+ }
+
+ const bool color_;
+};
+
+/// Allow using enum structs as bitflags
+#define MU_TO_NUM(ET, ELM) std::underlying_type_t<ET>(ELM)
+#define MU_TO_ENUM(ET, NUM) static_cast<ET>(NUM)
+#define MU_ENABLE_BITOPS(ET) \
+ constexpr ET operator&(ET e1, ET e2) \
+ { \
+ return MU_TO_ENUM(ET, MU_TO_NUM(ET, e1) & MU_TO_NUM(ET, e2)); \
+ } \
+ constexpr ET operator|(ET e1, ET e2) \
+ { \
+ return MU_TO_ENUM(ET, MU_TO_NUM(ET, e1) | MU_TO_NUM(ET, e2)); \
+ } \
+ constexpr ET operator~(ET e) { return MU_TO_ENUM(ET, ~(MU_TO_NUM(ET, e))); } \
+ constexpr bool any_of(ET e) { return MU_TO_NUM(ET, e) != 0; } \
+ constexpr bool none_of(ET e) { return MU_TO_NUM(ET, e) == 0; } \
+ constexpr bool one_of(ET e1, ET e2) { return (e1 & e2) == e2; } \
+ constexpr ET& operator&=(ET& e1, ET e2) { return e1 = e1 & e2; } \
+ constexpr ET& operator|=(ET& e1, ET e2) { return e1 = e1 | e2; } \
+ static_assert(1==1) // require a semicolon
+
+} // namespace Mu
+
+#endif /* __MU_UTILS_HH__ */
--- /dev/null
+/*
+** 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_XAPIAN_UTILS_HH__
+#define MU_XAPIAN_UTILS_HH__
+
+#include <xapian.h>
+#include <glib.h>
+#include "mu-result.hh"
+
+namespace Mu {
+
+// LCOV_EXCL_START
+
+// avoid exception-handling boilerplate.
+template <typename Func>
+void
+xapian_try(Func&& func) noexcept
+try {
+ func();
+} catch (const Xapian::Error& xerr) {
+ g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
+} catch (const std::runtime_error& re) {
+ g_critical("%s: error: %s", __func__, re.what());
+} catch (const std::exception& e) {
+ g_critical("%s: caught exception: %s", __func__, e.what());
+} catch (...) {
+ g_critical("%s: caught exception", __func__);
+}
+
+template <typename Func, typename Default = std::invoke_result<Func>>
+auto
+xapian_try(Func&& func, Default&& def) noexcept -> std::decay_t<decltype(func())>
+try {
+ return func();
+} catch (const Xapian::Error& xerr) {
+ g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
+ return static_cast<Default>(def);
+} catch (const std::runtime_error& re) {
+ g_critical("%s: error: %s", __func__, re.what());
+ return static_cast<Default>(def);
+} catch (const std::exception& e) {
+ g_critical("%s: caught exception: %s", __func__, e.what());
+ return static_cast<Default>(def);
+} catch (...) {
+ g_critical("%s: caught exception", __func__);
+ return static_cast<Default>(def);
+}
+
+
+template <typename Func>
+auto
+xapian_try_result(Func&& func) noexcept -> std::decay_t<decltype(func())>
+try {
+ return func();
+} catch (const Xapian::Error& xerr) {
+ return Err(Error::Code::Xapian, "%s", xerr.get_error_string());
+} catch (const std::runtime_error& re) {
+ return Err(Error::Code::Internal, "runtime error: %s", re.what());
+} catch (const std::exception& e) {
+ return Err(Error::Code::Internal, "caught exception: %s", e.what());
+} catch (...) {
+ return Err(Error::Code::Internal, "caught exception");
+}
+
+// LCOV_EXCL_STOP
+
+} // namespace Mu
+
+#endif /* MU_ XAPIAN_UTILS_HH__ */
--- /dev/null
+## 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-command-parser',
+ executable('test-command-parser',
+ 'test-command-parser.cc',
+ install: false,
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+test('test-mu-util',
+ executable('test-mu-util',
+ 'test-mu-util.c',
+ install: false,
+ dependencies: [glib_dep,config_h_dep, lib_mu_utils_dep]))
+test('test-option',
+ executable('test-option',
+ 'test-option.cc',
+ install: false,
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+test('test-mu-utils',
+ executable('test-mu-utils',
+ 'test-utils.cc',
+ install: false,
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+test('test-sexp',
+ executable('test-sexp',
+ 'test-sexp.cc',
+ install: false,
+ dependencies: [glib_dep, lib_mu_utils_dep] ))
--- /dev/null
+/*
+** 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.
+*/
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+
+#include "mu-command-parser.hh"
+#include "mu-utils.hh"
+#include "mu-test-utils.hh"
+
+using namespace Mu;
+
+static void
+test_param_getters()
+{
+ const auto sexp{Sexp::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")};
+
+ if (g_test_verbose())
+ std::cout << sexp << "\n";
+
+ g_assert_cmpint(Command::get_int_or(sexp.list(), ":bar"), ==, 123);
+ assert_equal(Command::get_string_or(sexp.list(), ":bra", "bla"), "bla");
+ assert_equal(Command::get_string_or(sexp.list(), ":cuux"), "456");
+
+ g_assert_true(Command::get_bool_or(sexp.list(), ":boo") == false);
+ g_assert_true(Command::get_bool_or(sexp.list(), ":bah") == true);
+}
+
+static bool
+call(const Command::CommandMap& cmap, const std::string& str)
+try {
+ const auto sexp{Sexp::make_parse(str)};
+ invoke(cmap, sexp);
+
+ return true;
+
+} catch (const Error& err) {
+ g_warning("%s", err.what());
+ return false;
+}
+
+static void
+test_command()
+{
+ using namespace Command;
+ allow_warnings();
+
+ CommandMap cmap;
+
+ cmap.emplace(
+ "my-command",
+ CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
+ {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
+ "My command,",
+ {}});
+
+ g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
+ g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
+
+ g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
+}
+
+static void
+test_command2()
+{
+ using namespace Command;
+ allow_warnings();
+
+ CommandMap cmap;
+ cmap.emplace("bla",
+ CommandInfo{ArgMap{
+ {":foo", ArgInfo{Sexp::Type::Number, false, "foo"}},
+ {":bar", ArgInfo{Sexp::Type::String, false, "bar"}},
+ },
+ "yeah",
+ [&](const auto& params) {}});
+
+ g_assert_true(call(cmap, "(bla :foo nil)"));
+ g_assert_false(call(cmap, "(bla :foo nil :bla nil)"));
+}
+
+static void
+test_command_fail()
+{
+ using namespace Command;
+
+ allow_warnings();
+
+ CommandMap cmap;
+
+ cmap.emplace(
+ "my-command",
+ CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
+ {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
+ "My command,",
+ {}});
+
+ g_assert_false(call(cmap, "(my-command)"));
+ g_assert_false(call(cmap, "(my-command2)"));
+ g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)"));
+ g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")"));
+}
+
+static void
+black_hole()
+{
+}
+
+int
+main(int argc, char* argv[]) try {
+
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/utils/command-parser/param-getters", test_param_getters);
+ g_test_add_func("/utils/command-parser/command", test_command);
+ g_test_add_func("/utils/command-parser/command2", test_command2);
+ g_test_add_func("/utils/command-parser/command-fail", test_command_fail);
+
+ 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();
+
+} catch (const std::runtime_error& re) {
+ std::cerr << re.what() << "\n";
+ return 1;
+}
--- /dev/null
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <locale.h>
+
+#include "mu-str.h"
+
+static void
+assert_cmplst (GSList *lst, const char *items[])
+{
+ int i;
+
+ if (!lst)
+ g_assert (!items);
+
+ for (i = 0; lst; lst = g_slist_next(lst), ++i)
+ g_assert_cmpstr ((char*)lst->data,==,items[i]);
+
+ g_assert (items[i] == NULL);
+}
+
+
+static GSList*
+create_list (const char *items[])
+{
+ GSList *lst;
+
+ lst = NULL;
+ while (items && *items) {
+ lst = g_slist_prepend (lst, g_strdup(*items));
+ ++items;
+ }
+
+ return g_slist_reverse (lst);
+
+}
+
+static void
+test_mu_str_from_list (void)
+{
+ {
+ const char *strs[] = {"aap", "noot", "mies", NULL};
+ GSList *lst = create_list (strs);
+ gchar *str = mu_str_from_list (lst, ',');
+ g_assert_cmpstr ("aap,noot,mies", ==, str);
+ mu_str_free_list (lst);
+ g_free (str);
+ }
+
+ {
+ const char *strs[] = {"aap", "no,ot", "mies", NULL};
+ GSList *lst = create_list (strs);
+ gchar *str = mu_str_from_list (lst, ',');
+ g_assert_cmpstr ("aap,no,ot,mies", ==, str);
+ mu_str_free_list (lst);
+ g_free (str);
+ }
+
+ {
+ const char *strs[] = {NULL};
+ GSList *lst = create_list (strs);
+ gchar *str = mu_str_from_list (lst,'@');
+ g_assert_cmpstr (NULL, ==, str);
+ mu_str_free_list (lst);
+ g_free (str);
+ }
+
+
+}
+
+
+static void
+test_mu_str_to_list (void)
+{
+ {
+ const char *items[]= {"foo", "bar ", "cuux", NULL};
+ GSList *lst = mu_str_to_list ("foo@bar @cuux",'@', FALSE);
+ assert_cmplst (lst, items);
+ mu_str_free_list (lst);
+ }
+
+ {
+ GSList *lst = mu_str_to_list (NULL,'x',FALSE);
+ g_assert (lst == NULL);
+ mu_str_free_list (lst);
+ }
+}
+
+static void
+test_mu_str_to_list_strip (void)
+{
+ const char *items[]= {"foo", "bar", "cuux", NULL};
+ GSList *lst = mu_str_to_list ("foo@bar @cuux",'@', TRUE);
+ assert_cmplst (lst, items);
+ mu_str_free_list (lst);
+}
+
+static void
+test_mu_str_remove_ctrl_in_place (void)
+{
+ unsigned u;
+ struct {
+ char *str;
+ const char *exp;
+ } strings [] = {
+ { g_strdup(""), ""},
+ { g_strdup("hello, world!"), "hello, world!" },
+ { g_strdup("hello,\tworld!"), "hello, world!" },
+ { g_strdup("hello,\n\nworld!"), "hello, world!", },
+ { g_strdup("hello,\x1f\x1e\x1ew\nor\nld!"), "hello,w or ld!" },
+ { g_strdup("\x1ehello, world!\x1f"), "hello, world!" }
+ };
+
+ for (u = 0; u != G_N_ELEMENTS(strings); ++u) {
+ char *res;
+ res = mu_str_remove_ctrl_in_place (strings[u].str);
+ g_assert_cmpstr (res,==,strings[u].exp);
+ g_free (strings[u].str);
+ }
+}
+
+
+int
+main (int argc, char *argv[])
+{
+ setlocale (LC_ALL, "");
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/mu-str/mu-str-from-list",
+ test_mu_str_from_list);
+ g_test_add_func ("/mu-str/mu-str-to-list",
+ test_mu_str_to_list);
+ g_test_add_func ("/mu-str/mu-str-to-list-strip",
+ test_mu_str_to_list_strip);
+
+ g_test_add_func ("/mu-str/mu_str_remove_ctrl_in_place",
+ test_mu_str_remove_ctrl_in_place);
+
+
+ return g_test_run ();
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+
+#include "mu-util.h"
+
+static void
+test_mu_util_dir_expand_00(void)
+{
+#ifdef HAVE_WORDEXP_H
+ gchar *got, *expected;
+
+ got = mu_util_dir_expand("~/IProbablyDoNotExist");
+ expected = g_strdup_printf("%s%cIProbablyDoNotExist",
+ getenv("HOME"), G_DIR_SEPARATOR);
+
+ g_assert_cmpstr(got, ==, expected);
+
+ g_free(got);
+ g_free(expected);
+#endif /*HAVE_WORDEXP_H*/
+}
+
+static void
+test_mu_util_dir_expand_01(void)
+{
+ /* XXXX: the testcase does not work when using some dir
+ * setups; (see issue #585), although the code should still
+ * work. Turn of the test for now */
+ return;
+
+#ifdef HAVE_WORDEXP_H
+ {
+ gchar *got, *expected;
+
+ got = mu_util_dir_expand("~/Desktop");
+ expected = g_strdup_printf("%s%cDesktop",
+ getenv("HOME"), G_DIR_SEPARATOR);
+
+ g_assert_cmpstr(got, ==, expected);
+
+ g_free(got);
+ g_free(expected);
+ }
+#endif /*HAVE_WORDEXP_H*/
+}
+
+static void
+test_mu_util_guess_maildir_01(void)
+{
+ char* got;
+ const char* expected;
+
+ /* skip the test if there's no /tmp */
+ if (access("/tmp", F_OK))
+ return;
+
+ g_setenv("MAILDIR", "/tmp", TRUE);
+
+ got = mu_util_guess_maildir();
+ expected = "/tmp";
+
+ g_assert_cmpstr(got, ==, expected);
+ g_free(got);
+}
+
+static void
+test_mu_util_guess_maildir_02(void)
+{
+ char *got, *mdir;
+
+ g_unsetenv("MAILDIR");
+
+ mdir = g_strdup_printf("%s%cMaildir",
+ getenv("HOME"), G_DIR_SEPARATOR);
+ got = mu_util_guess_maildir();
+
+ if (access(mdir, F_OK) == 0)
+ g_assert_cmpstr(got, ==, mdir);
+ else
+ g_assert_cmpstr(got, ==, NULL);
+
+ g_free(got);
+ g_free(mdir);
+}
+
+static void
+test_mu_util_check_dir_01(void)
+{
+ if (g_access("/usr/bin", F_OK) == 0) {
+ g_assert_cmpuint(
+ mu_util_check_dir("/usr/bin", TRUE, FALSE) == TRUE,
+ ==,
+ g_access("/usr/bin", R_OK) == 0);
+ }
+}
+
+static void
+test_mu_util_check_dir_02(void)
+{
+ if (g_access("/tmp", F_OK) == 0) {
+ g_assert_cmpuint(
+ mu_util_check_dir("/tmp", FALSE, TRUE) == TRUE,
+ ==,
+ g_access("/tmp", W_OK) == 0);
+ }
+}
+
+static void
+test_mu_util_check_dir_03(void)
+{
+ if (g_access(".", F_OK) == 0) {
+ g_assert_cmpuint(
+ mu_util_check_dir(".", TRUE, TRUE) == TRUE,
+ ==,
+ g_access(".", W_OK | R_OK) == 0);
+ }
+}
+
+static void
+test_mu_util_check_dir_04(void)
+{
+ /* not a dir, so it must be false */
+ g_assert_cmpuint(
+ mu_util_check_dir("test-util.c", TRUE, TRUE),
+ ==,
+ FALSE);
+}
+
+static void
+test_mu_util_get_dtype_with_lstat(void)
+{
+ g_assert_cmpuint(
+ mu_util_get_dtype(MU_TESTMAILDIR, TRUE), ==, DT_DIR);
+ g_assert_cmpuint(
+ mu_util_get_dtype(MU_TESTMAILDIR2, TRUE), ==, DT_DIR);
+ g_assert_cmpuint(
+ mu_util_get_dtype(MU_TESTMAILDIR2 "/Foo/cur/mail5", TRUE),
+ ==, DT_REG);
+}
+
+static void
+test_mu_util_supports(void)
+{
+ gboolean has_guile;
+ gchar* path;
+
+#ifdef BUILD_GUILE
+ has_guile = TRUE;
+#else
+ has_guile = FALSE;
+#endif /*BUILD_GUILE*/
+
+ g_assert_cmpuint(mu_util_supports(MU_FEATURE_GUILE), ==, has_guile);
+
+ path = g_find_program_in_path("gnuplot");
+ g_free(path);
+
+ g_assert_cmpuint(mu_util_supports(MU_FEATURE_GNUPLOT), ==,
+ path ? TRUE : FALSE);
+
+ g_assert_cmpuint(
+ mu_util_supports(MU_FEATURE_GNUPLOT | MU_FEATURE_GUILE),
+ ==,
+ has_guile && path ? TRUE : FALSE);
+}
+
+static void
+test_mu_util_program_in_path(void)
+{
+ g_assert_cmpuint(mu_util_program_in_path("ls"), ==, TRUE);
+}
+
+
+static void
+test_mu_util_summarize(void)
+{
+ const char *txt =
+ "Khiron was fortified and made the seat of a pargana during "
+ "the reign of Asaf-ud-Daula.\n\the headquarters had previously "
+ "been at Satanpur since its foundation and fortification by "
+ "the Bais raja Sathna.\n\nKhiron was also historically the seat "
+ "of a taluqdari estate belonging to a Janwar dynasty.\n"
+ "There were also several Kayasth qanungo families, "
+ "including many descended from Rai Sahib Rai, who had been "
+ "a chakladar under the Nawabs of Awadh.";
+
+ char *summ = mu_str_summarize(txt, 3);
+ g_assert_cmpstr(summ, ==,
+ "Khiron was fortified and made the seat of a pargana "
+ "during the reign of Asaf-ud-Daula. he headquarters had "
+ "previously been at Satanpur since its foundation and "
+ "fortification by the Bais raja Sathna. ");
+ g_free (summ);
+}
+
+
+static void
+test_mu_error(void)
+{
+ GQuark q;
+ GError *err;
+ gboolean res;
+
+ q = mu_util_error_quark();
+ g_assert_true(q != 0);
+
+
+ err = NULL;
+ res = mu_util_g_set_error(&err, MU_ERROR_IN_PARAMETERS,
+ "Hello, %s!", "World");
+
+ g_assert_false(res);
+ g_assert_cmpuint(err->domain, ==, q);
+ g_assert_cmpuint(err->code, ==, MU_ERROR_IN_PARAMETERS);
+ g_assert_cmpstr(err->message,==,"Hello, World!");
+
+ g_clear_error(&err);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ /* mu_util_dir_expand */
+ g_test_add_func("/mu-util/mu-util-dir-expand-00",
+ test_mu_util_dir_expand_00);
+ g_test_add_func("/mu-util/mu-util-dir-expand-01",
+ test_mu_util_dir_expand_01);
+
+ /* mu_util_guess_maildir */
+ g_test_add_func("/mu-util/mu-util-guess-maildir-01",
+ test_mu_util_guess_maildir_01);
+ g_test_add_func("/mu-util/mu-util-guess-maildir-02",
+ test_mu_util_guess_maildir_02);
+
+ /* mu_util_check_dir */
+ g_test_add_func("/mu-util/mu-util-check-dir-01",
+ test_mu_util_check_dir_01);
+ g_test_add_func("/mu-util/mu-util-check-dir-02",
+ test_mu_util_check_dir_02);
+ g_test_add_func("/mu-util/mu-util-check-dir-03",
+ test_mu_util_check_dir_03);
+ g_test_add_func("/mu-util/mu-util-check-dir-04",
+ test_mu_util_check_dir_04);
+
+ g_test_add_func("/mu-util/mu-util-get-dtype-with-lstat",
+ test_mu_util_get_dtype_with_lstat);
+
+ g_test_add_func("/mu-util/mu-util-supports", test_mu_util_supports);
+ g_test_add_func("/mu-util/mu-util-program-in-path",
+ test_mu_util_program_in_path);
+
+ g_test_add_func("/mu-util/summarize", test_mu_util_summarize);
+ g_test_add_func("/mu-util/error", test_mu_error);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** 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-utils.hh"
+#include "mu-option.hh"
+
+using namespace Mu;
+
+static Option<int>
+get_opt_int(bool b)
+{
+ if (b)
+ return Some(123);
+ else
+ return Nothing;
+}
+
+static void
+test_option()
+{
+ {
+ const auto oi{get_opt_int(true)};
+ g_assert_true(!!oi);
+ g_assert_cmpint(oi.value(), ==, 123);
+ }
+
+ {
+ const auto oi{get_opt_int(false)};
+ g_assert_false(!!oi);
+ g_assert_false(oi.has_value());
+ g_assert_cmpint(oi.value_or(456), ==, 456);
+ }
+}
+
+int
+main(int argc, char* argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add_func("/option/option", test_option);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** 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 <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+
+#include "mu-command-parser.hh"
+#include "mu-utils.hh"
+#include "mu-test-utils.hh"
+
+using namespace Mu;
+
+static bool
+check_parse(const std::string& expr, const std::string& expected)
+{
+ try {
+ const auto parsed{to_string(Sexp::make_parse(expr))};
+ assert_equal(parsed, expected);
+ return true;
+
+ } catch (const Error& err) {
+ g_warning("caught exception parsing '%s': %s", expr.c_str(), err.what());
+ return false;
+ }
+}
+
+static void
+test_parser()
+{
+ check_parse(":foo-123", ":foo-123");
+ check_parse("foo", "foo");
+ check_parse(R"(12345)", "12345");
+ check_parse(R"(-12345)", "-12345");
+ check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")");
+
+ check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\"");
+
+ check_parse(R"("foo
+bar")",
+ "\"foo\nbar\"");
+}
+
+static void
+test_list()
+{
+ const auto nstr{Sexp::make_string("foo")};
+ g_assert_true(nstr.value() == "foo");
+ g_assert_true(nstr.type() == Sexp::Type::String);
+ assert_equal(nstr.to_sexp_string(), "\"foo\"");
+
+ const auto nnum{Sexp::make_number(123)};
+ g_assert_true(nnum.value() == "123");
+ g_assert_true(nnum.type() == Sexp::Type::Number);
+ assert_equal(nnum.to_sexp_string(), "123");
+
+ const auto nsym{Sexp::make_symbol("blub")};
+ g_assert_true(nsym.value() == "blub");
+ g_assert_true(nsym.type() == Sexp::Type::Symbol);
+ assert_equal(nsym.to_sexp_string(), "blub");
+
+ Sexp::List list;
+ list.add(Sexp::make_string("foo"))
+ .add(Sexp::make_number(123))
+ .add(Sexp::make_symbol("blub"));
+
+ const auto nlst = Sexp::make_list(std::move(list));
+ g_assert_true(nlst.list().size() == 3);
+ g_assert_true(nlst.type() == Sexp::Type::List);
+ g_assert_true(nlst.list().at(1).value() == "123");
+
+ assert_equal(nlst.to_sexp_string(), "(\"foo\" 123 blub)");
+}
+
+static void
+test_prop_list()
+{
+ Sexp::List l1;
+ l1.add_prop(":foo", Sexp::make_string("bar"));
+ Sexp s2{Sexp::make_list(std::move(l1))};
+ assert_equal(s2.to_sexp_string(), "(:foo \"bar\")");
+ g_assert_true(s2.is_prop_list());
+
+ Sexp::List l2;
+ const std::string x{"bar"};
+ l2.add_prop(":foo", Sexp::make_string(x));
+ l2.add_prop(":bar", Sexp::make_number(77));
+ Sexp::List l3;
+ l3.add_prop(":cuux", Sexp::make_list(std::move(l2)));
+ Sexp s3{Sexp::make_list(std::move(l3))};
+ assert_equal(s3.to_sexp_string(), "(:cuux (:foo \"bar\" :bar 77))");
+}
+
+static void
+test_props()
+{
+ auto sexp2 = Sexp::make_list(Sexp::make_string("foo"),
+ Sexp::make_number(123),
+ Sexp::make_symbol("blub"));
+
+ auto sexp = Sexp::make_prop_list(":foo",
+ Sexp::make_string("bär"),
+ ":cuux",
+ Sexp::make_number(123),
+ ":flub",
+ Sexp::make_symbol("fnord"),
+ ":boo",
+ std::move(sexp2));
+
+ assert_equal(sexp.to_sexp_string(),
+ "(:foo \"b\303\244r\" :cuux 123 :flub fnord :boo (\"foo\" 123 blub))");
+}
+
+static void
+test_prop_list_remove()
+{
+ {
+ Sexp::List lst;
+ lst.add_prop(":foo", Sexp::make_string("123"))
+ .add_prop(":bar", Sexp::make_number(123));
+
+ assert_equal(Sexp::make_list(std::move(lst)).to_sexp_string(),
+ R"((:foo "123" :bar 123))");
+ }
+
+ {
+ Sexp::List lst;
+ lst.add_prop(":foo", Sexp::make_string("123"))
+ .add_prop(":bar", Sexp::make_number(123));
+
+ assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(),
+ R"((:foo "123" :bar 123))");
+
+ lst.remove_prop(":bar");
+
+ assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(),
+ R"((:foo "123"))");
+
+ lst.clear();
+ g_assert_cmpuint(lst.size(), ==, 0);
+ }
+
+ {
+ Sexp::List lst;
+ lst.add(Sexp::make_number(123));
+ Sexp s2{Sexp::make_list(std::move(lst))};
+ g_assert_false(s2.is_prop_list());
+ }
+}
+
+int
+main(int argc, char* argv[])
+try {
+ mu_test_init(&argc, &argv);
+
+ if (argc == 2) {
+ std::cout << Sexp::make_parse(argv[1]) << '\n';
+ return 0;
+ }
+
+ g_test_add_func("/utils/sexp/parser", test_parser);
+ g_test_add_func("/utils/sexp/list", test_list);
+ g_test_add_func("/utils/sexp/proplist", test_prop_list);
+ g_test_add_func("/utils/sexp/proplist-remove", test_prop_list_remove);
+ g_test_add_func("/utils/sexp/props", test_props);
+
+ return g_test_run();
+
+} catch (const std::runtime_error& re) {
+ std::cerr << re.what() << "\n";
+ return 1;
+}
--- /dev/null
+/*
+** 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 <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+#include <functional>
+#include <array>
+
+#include "mu-utils.hh"
+#include "mu-test-utils.hh"
+#include "mu-error.hh"
+
+using namespace Mu;
+
+
+struct Case {
+ const std::string expr;
+ bool is_first{};
+ const std::string expected;
+};
+using CaseVec = std::vector<Case>;
+using ProcFunc = std::function<std::string(std::string, bool)>;
+
+static void
+test_cases(const CaseVec& cases, ProcFunc proc)
+{
+ for (const auto& casus : cases) {
+ const auto res = proc(casus.expr, casus.is_first);
+ if (g_test_verbose()) {
+ std::cout << "\n";
+ std::cout << casus.expr << ' ' << casus.is_first << std::endl;
+ std::cout << "exp: '" << casus.expected << "'" << std::endl;
+ std::cout << "got: '" << res << "'" << std::endl;
+ }
+
+ g_assert_true(casus.expected == res);
+ }
+}
+
+static void
+test_date_basic()
+{
+ const auto hki = "Europe/Helsinki";
+
+ // ensure we have the needed TZ or skip the test.
+ if (!timezone_available(hki)) {
+ g_test_skip("timezone Europe/Helsinki not available");
+ return;
+ }
+
+ g_setenv("TZ", hki, TRUE);
+ constexpr std::array<std::tuple<const char*, bool/*is_first*/, int64_t>, 13> cases = {{
+ {"2015-09-18T09:10:23", true, 1442556623},
+ {"1972-12-14T09:10:23", true, 93165023},
+ {"1854-11-18T17:10:23", true, 0},
+
+ {"2000-02-31T09:10:23", true, 951861599},
+ {"2000-02-29T23:59:59", true, 951861599},
+
+ {"20220602", true, 1654117200},
+ {"20220605", false, 1654462799},
+
+ {"202206", true, 1654030800},
+ {"202206", false, 1656622799},
+
+ {"2016", true, 1451599200},
+ {"2016", false, 1483221599},
+
+ // {"fnorb", true, -1},
+ // {"fnorb", false, -1},
+ {"", false, G_MAXINT64},
+ {"", true, 0}
+ }};
+
+ for (auto& test: cases) {
+ if (g_test_verbose())
+ g_debug("checking %s", std::get<0>(test));
+ g_assert_cmpuint(parse_date_time(std::get<0>(test),
+ std::get<1>(test)).value_or(-1),==,
+ std::get<2>(test));
+ }
+}
+
+static void
+test_date_ymwdhMs(void)
+{
+ struct testcase {
+ std::string expr;
+ int64_t diff;
+ int tolerance;
+ };
+
+ std::array<testcase, 7> cases = {{
+ {"7s", 7, 1},
+ {"3M", 3 * 60, 1},
+ {"3h", 3 * 60 * 60, 1},
+ {"21d", 21 * 24 * 60 * 60, 3600 + 1},
+ {"2w", 2 * 7 * 24 * 60 * 60, 3600 + 1},
+ {"2y", 2 * 365 * 24 * 60 * 60, 24 * 3600 + 1},
+ {"3m", 3 * 30 * 24 * 60 * 60, 3 * 24 * 3600 + 1}
+ }};
+
+ for (auto&& tcase: cases) {
+ const auto date = parse_date_time(tcase.expr, true);
+ g_assert_true(date);
+ const auto diff = ::time({}) - *date;
+ if (g_test_verbose())
+ std::cerr << tcase.expr << ' ' << diff << ' ' << tcase.diff << '\n';
+
+ g_assert_true(tcase.diff - diff <= tcase.tolerance);
+ }
+
+ // note: perhaps it'd be nice if we'd detect this error;
+ // currently we're being rather tolerant
+ // g_assert_false(!!parse_date_time("25q", false));
+}
+
+static void
+test_parse_size()
+{
+ constexpr std::array<std::tuple<const char*, bool, int64_t>, 6> cases = {{
+ { "456", false, 456 },
+ { "", false, G_MAXINT64 },
+ { "", true, 0 },
+ { "2K", false, 2048 },
+ { "2M", true, 2097152 },
+ { "5G", true, 5368709120 }
+ }};
+ for(auto&& test: cases) {
+ g_assert_cmpint(parse_size(std::get<0>(test), std::get<1>(test))
+ .value_or(-1), ==, std::get<2>(test));
+ }
+
+ g_assert_false(!!parse_size("-1", true));
+ g_assert_false(!!parse_size("scoobydoobydoo", false));
+}
+
+static void
+test_flatten()
+{
+ CaseVec cases = {
+ {"Менделе́ев", true, "менделеев"},
+ {"", false, ""},
+ {"Ångström", true, "angstrom"},
+ };
+
+ test_cases(cases, [](auto s, auto f) { return utf8_flatten(s); });
+}
+
+static void
+test_remove_ctrl()
+{
+ CaseVec cases = {
+ {"Foo\n\nbar", true, "Foo bar"},
+ {"", false, ""},
+ {" ", false, " "},
+ {"Hello World ", false, "Hello World "},
+ {"Ångström", false, "Ångström"},
+ };
+
+ test_cases(cases, [](auto s, auto f) { return remove_ctrl(s); });
+}
+
+static void
+test_clean()
+{
+ CaseVec cases = {
+ {"\t a\t\nb ", true, "a b"},
+ {"", false, ""},
+ {"Ångström", true, "Ångström"},
+ };
+
+ test_cases(cases, [](auto s, auto f) { return utf8_clean(s); });
+}
+
+static void
+test_format()
+{
+ g_assert_true(format("hello %s", "world") == "hello world");
+ g_assert_true(format("hello %s, %u", "world", 123) == "hello world, 123");
+}
+
+static void
+test_split()
+{
+ using svec = std::vector<std::string>;
+ auto assert_equal_svec=[](const svec& sv1, const svec& sv2) {
+ g_assert_cmpuint(sv1.size(),==,sv2.size());
+ for (auto i = 0U; i != sv1.size(); ++i)
+ g_assert_cmpstr(sv1[i].c_str(),==,sv2[i].c_str());
+ };
+
+ // string sepa
+ assert_equal_svec(split("axbxc", "x"), {"a", "b", "c"});
+ assert_equal_svec(split("axbxcx", "x"), {"a", "b", "c", ""});
+ assert_equal_svec(split("", "boo"), {});
+ assert_equal_svec(split("ayybyyc", "yy"), {"a", "b", "c"});
+ assert_equal_svec(split("abc", ""), {"a", "b", "c"});
+ assert_equal_svec(split("", "boo"), {});
+
+ // char sepa
+ assert_equal_svec(split("axbxc", 'x'), {"a", "b", "c"});
+ assert_equal_svec(split("axbxcx", 'x'), {"a", "b", "c", ""});
+
+ // rx sexp
+ assert_equal_svec(split("axbyc", std::regex("[xy]")), {"a", "b", "c"});
+}
+
+static void
+test_join()
+{
+ assert_equal(join({"a", "b", "c"}, "x"), "axbxc");
+ assert_equal(join({"a", "b", "c"}, ""), "abc");
+ assert_equal(join({},"foo"), "");
+ assert_equal(join({"d", "e", "f"}, "foo"), "dfooefoof");
+}
+
+
+enum struct Bits { None = 0, Bit1 = 1 << 0, Bit2 = 1 << 1 };
+MU_ENABLE_BITOPS(Bits);
+
+static void
+test_define_bitmap()
+{
+ g_assert_cmpuint((guint)Bits::None, ==, (guint)0);
+ g_assert_cmpuint((guint)Bits::Bit1, ==, (guint)1);
+ g_assert_cmpuint((guint)Bits::Bit2, ==, (guint)2);
+
+ g_assert_cmpuint((guint)(Bits::Bit1 | Bits::Bit2), ==, (guint)3);
+ g_assert_cmpuint((guint)(Bits::Bit1 & Bits::Bit2), ==, (guint)0);
+
+ g_assert_cmpuint((guint)(Bits::Bit1 & (~Bits::Bit2)), ==, (guint)1);
+
+ {
+ Bits b{Bits::Bit1};
+ b |= Bits::Bit2;
+ g_assert_cmpuint((guint)b, ==, (guint)3);
+ }
+
+ {
+ Bits b{Bits::Bit1};
+ b &= Bits::Bit1;
+ g_assert_cmpuint((guint)b, ==, (guint)1);
+ }
+}
+
+static void
+test_to_from_lexnum()
+{
+ assert_equal(to_lexnum(0), "g0");
+ assert_equal(to_lexnum(100), "h64");
+ assert_equal(to_lexnum(12345), "j3039");
+
+ g_assert_cmpuint(from_lexnum(to_lexnum(0)), ==, 0);
+ g_assert_cmpuint(from_lexnum(to_lexnum(7777)), ==, 7777);
+ g_assert_cmpuint(from_lexnum(to_lexnum(9876543)), ==, 9876543);
+}
+
+static void
+test_locale_workaround()
+{
+ g_assert_true(locale_workaround());
+
+ g_setenv("LC_ALL", "BOO", 1);
+
+ g_assert_true(locale_workaround());
+}
+
+static void
+test_error()
+{
+ GError *err;
+ err = g_error_new(MU_ERROR_DOMAIN, 77, "Hello, %s", "world");
+ Error ex{Error::Code::Crypto, &err, "boo"};
+ g_assert_cmpstr(ex.what(), ==, "boo: Hello, world");
+
+ ex.fill_g_error(&err);
+ g_assert_cmpuint(err->code, ==, static_cast<unsigned>(Error::Code::Crypto));
+ g_clear_error(&err);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/utils/date-basic", test_date_basic);
+ g_test_add_func("/utils/date-ymwdhMs", test_date_ymwdhMs);
+ g_test_add_func("/utils/parse-size", test_parse_size);
+ g_test_add_func("/utils/flatten", test_flatten);
+ g_test_add_func("/utils/remove-ctrl", test_remove_ctrl);
+ g_test_add_func("/utils/clean", test_clean);
+ g_test_add_func("/utils/format", test_format);
+ g_test_add_func("/utils/split", test_split);
+ g_test_add_func("/utils/join", test_join);
+ g_test_add_func("/utils/define-bitmap", test_define_bitmap);
+ g_test_add_func("/utils/to-from-lexnum", test_to_from_lexnum);
+ g_test_add_func("/utils/locale-workaround", test_locale_workaround);
+ g_test_add_func("/utils/error", test_error);
+
+ return g_test_run();
+}
--- /dev/null
+## 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 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+EXTRA_DIST= \
+ ax_ac_append_to_file.m4 \
+ ax_ac_print_to_file.m4 \
+ ax_add_am_macro_static.m4 \
+ ax_am_macros_static.m4 \
+ ax_append_compile_flags.m4 \
+ ax_append_flag.m4 \
+ ax_append_link_flags.m4 \
+ ax_check_compile_flag.m4 \
+ ax_check_enable_debug.m4 \
+ ax_check_gnu_make.m4 \
+ ax_check_link_flag.m4 \
+ ax_code_coverage.m4 \
+ ax_compiler_flags.m4 \
+ ax_compiler_flags_cflags.m4 \
+ ax_compiler_flags_cxxflags.m4 \
+ ax_compiler_flags_gir.m4 \
+ ax_compiler_flags_ldflags.m4 \
+ ax_cxx_compile_stdcxx.m4 \
+ ax_cxx_compile_stdcxx_17.m4 \
+ ax_file_escapes.m4 \
+ ax_is_release.m4 \
+ ax_lib_readline.m4 \
+ ax_require_defined.m4 \
+ ax_valgrind_check.m4 \
+ guile.m4 \
+ lib-ld.m4 \
+ lib-link.m4 \
+ lib-prefix.m4
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_ac_append_to_file.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_AC_APPEND_TO_FILE([FILE],[DATA])
+#
+# DESCRIPTION
+#
+# Appends the specified data to the specified Autoconf is run. If you want
+# to append to a file when configure is run use AX_APPEND_TO_FILE instead.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 10
+
+AC_DEFUN([AX_AC_APPEND_TO_FILE],[
+AC_REQUIRE([AX_FILE_ESCAPES])
+m4_esyscmd(
+AX_FILE_ESCAPES
+[
+printf "%s" "$2" >> "$1"
+])
+])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_ac_print_to_file.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_AC_PRINT_TO_FILE([FILE],[DATA])
+#
+# DESCRIPTION
+#
+# Writes the specified data to the specified file when Autoconf is run. If
+# you want to print to a file when configure is run use AX_PRINT_TO_FILE
+# instead.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 10
+
+AC_DEFUN([AX_AC_PRINT_TO_FILE],[
+m4_esyscmd(
+AC_REQUIRE([AX_FILE_ESCAPES])
+[
+printf "%s" "$2" > "$1"
+])
+])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_add_am_macro_static.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_ADD_AM_MACRO_STATIC([RULE])
+#
+# DESCRIPTION
+#
+# Adds the specified rule to $AMINCLUDE.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Tom Howard <tomhoward@users.sf.net>
+# Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 8
+
+AC_DEFUN([AX_ADD_AM_MACRO_STATIC],[
+ AC_REQUIRE([AX_AM_MACROS_STATIC])
+ AX_AC_APPEND_TO_FILE(AMINCLUDE_STATIC,[$1])
+])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_am_macros_static.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_AM_MACROS_STATIC
+#
+# DESCRIPTION
+#
+# Adds support for macros that create Automake rules. You must manually
+# add the following line
+#
+# include $(top_srcdir)/aminclude_static.am
+#
+# to your Makefile.am files.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Tom Howard <tomhoward@users.sf.net>
+# Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 11
+
+AC_DEFUN([AMINCLUDE_STATIC],[aminclude_static.am])
+
+AC_DEFUN([AX_AM_MACROS_STATIC],
+[
+AX_AC_PRINT_TO_FILE(AMINCLUDE_STATIC,[
+# ]AMINCLUDE_STATIC[ generated automatically by Autoconf
+# from AX_AM_MACROS_STATIC on ]m4_esyscmd([LC_ALL=C date])[
+])
+])
--- /dev/null
+# ============================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_append_compile_flags.html
+# ============================================================================
+#
+# SYNOPSIS
+#
+# AX_APPEND_COMPILE_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+# For every FLAG1, FLAG2 it is checked whether the compiler works with the
+# flag. If it does, the flag is added FLAGS-VARIABLE
+#
+# If FLAGS-VARIABLE is not specified, the current language's flags (e.g.
+# CFLAGS) is used. During the check the flag is always added to the
+# current language's flags.
+#
+# If EXTRA-FLAGS is defined, it is added to the current language's default
+# flags (e.g. CFLAGS) when the check is done. The check is thus made with
+# the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to
+# force the compiler to issue an error when a bad flag is given.
+#
+# INPUT gives an alternative input source to AC_COMPILE_IFELSE.
+#
+# NOTE: This macro depends on the AX_APPEND_FLAG and
+# AX_CHECK_COMPILE_FLAG. Please keep this macro in sync with
+# AX_APPEND_LINK_FLAGS.
+#
+# LICENSE
+#
+# Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 7
+
+AC_DEFUN([AX_APPEND_COMPILE_FLAGS],
+[AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG])
+AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+for flag in $1; do
+ AX_CHECK_COMPILE_FLAG([$flag], [AX_APPEND_FLAG([$flag], [$2])], [], [$3], [$4])
+done
+])dnl AX_APPEND_COMPILE_FLAGS
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_append_flag.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_APPEND_FLAG(FLAG, [FLAGS-VARIABLE])
+#
+# DESCRIPTION
+#
+# FLAG is appended to the FLAGS-VARIABLE shell variable, with a space
+# added in between.
+#
+# If FLAGS-VARIABLE is not specified, the current language's flags (e.g.
+# CFLAGS) is used. FLAGS-VARIABLE is not changed if it already contains
+# FLAG. If FLAGS-VARIABLE is unset in the shell, it is set to exactly
+# FLAG.
+#
+# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
+# Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 8
+
+AC_DEFUN([AX_APPEND_FLAG],
+[dnl
+AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_SET_IF
+AS_VAR_PUSHDEF([FLAGS], [m4_default($2,_AC_LANG_PREFIX[FLAGS])])
+AS_VAR_SET_IF(FLAGS,[
+ AS_CASE([" AS_VAR_GET(FLAGS) "],
+ [*" $1 "*], [AC_RUN_LOG([: FLAGS already contains $1])],
+ [
+ AS_VAR_APPEND(FLAGS,[" $1"])
+ AC_RUN_LOG([: FLAGS="$FLAGS"])
+ ])
+ ],
+ [
+ AS_VAR_SET(FLAGS,[$1])
+ AC_RUN_LOG([: FLAGS="$FLAGS"])
+ ])
+AS_VAR_POPDEF([FLAGS])dnl
+])dnl AX_APPEND_FLAG
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_append_link_flags.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_APPEND_LINK_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+# For every FLAG1, FLAG2 it is checked whether the linker works with the
+# flag. If it does, the flag is added FLAGS-VARIABLE
+#
+# If FLAGS-VARIABLE is not specified, the linker's flags (LDFLAGS) is
+# used. During the check the flag is always added to the linker's flags.
+#
+# If EXTRA-FLAGS is defined, it is added to the linker's default flags
+# when the check is done. The check is thus made with the flags: "LDFLAGS
+# EXTRA-FLAGS FLAG". This can for example be used to force the linker to
+# issue an error when a bad flag is given.
+#
+# INPUT gives an alternative input source to AC_COMPILE_IFELSE.
+#
+# NOTE: This macro depends on the AX_APPEND_FLAG and AX_CHECK_LINK_FLAG.
+# Please keep this macro in sync with AX_APPEND_COMPILE_FLAGS.
+#
+# LICENSE
+#
+# Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 7
+
+AC_DEFUN([AX_APPEND_LINK_FLAGS],
+[AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG])
+AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+for flag in $1; do
+ AX_CHECK_LINK_FLAG([$flag], [AX_APPEND_FLAG([$flag], [m4_default([$2], [LDFLAGS])])], [], [$3], [$4])
+done
+])dnl AX_APPEND_LINK_FLAGS
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_check_compile_flag.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CHECK_COMPILE_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+# Check whether the given FLAG works with the current language's compiler
+# or gives an error. (Warnings, however, are ignored)
+#
+# ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on
+# success/failure.
+#
+# If EXTRA-FLAGS is defined, it is added to the current language's default
+# flags (e.g. CFLAGS) when the check is done. The check is thus made with
+# the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to
+# force the compiler to issue an error when a bad flag is given.
+#
+# INPUT gives an alternative input source to AC_COMPILE_IFELSE.
+#
+# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this
+# macro in sync with AX_CHECK_{PREPROC,LINK}_FLAG.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
+# Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 6
+
+AC_DEFUN([AX_CHECK_COMPILE_FLAG],
+[AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF
+AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_[]_AC_LANG_ABBREV[]flags_$4_$1])dnl
+AC_CACHE_CHECK([whether _AC_LANG compiler accepts $1], CACHEVAR, [
+ ax_check_save_flags=$[]_AC_LANG_PREFIX[]FLAGS
+ _AC_LANG_PREFIX[]FLAGS="$[]_AC_LANG_PREFIX[]FLAGS $4 $1"
+ AC_COMPILE_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])],
+ [AS_VAR_SET(CACHEVAR,[yes])],
+ [AS_VAR_SET(CACHEVAR,[no])])
+ _AC_LANG_PREFIX[]FLAGS=$ax_check_save_flags])
+AS_VAR_IF(CACHEVAR,yes,
+ [m4_default([$2], :)],
+ [m4_default([$3], :)])
+AS_VAR_POPDEF([CACHEVAR])dnl
+])dnl AX_CHECK_COMPILE_FLAGS
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_check_enable_debug.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CHECK_ENABLE_DEBUG([enable by default=yes/info/profile/no], [ENABLE DEBUG VARIABLES ...], [DISABLE DEBUG VARIABLES NDEBUG ...], [IS-RELEASE])
+#
+# DESCRIPTION
+#
+# Check for the presence of an --enable-debug option to configure, with
+# the specified default value used when the option is not present. Return
+# the value in the variable $ax_enable_debug.
+#
+# Specifying 'yes' adds '-g -O0' to the compilation flags for all
+# languages. Specifying 'info' adds '-g' to the compilation flags.
+# Specifying 'profile' adds '-g -pg' to the compilation flags and '-pg' to
+# the linking flags. Otherwise, nothing is added.
+#
+# Define the variables listed in the second argument if debug is enabled,
+# defaulting to no variables. Defines the variables listed in the third
+# argument if debug is disabled, defaulting to NDEBUG. All lists of
+# variables should be space-separated.
+#
+# If debug is not enabled, ensure AC_PROG_* will not add debugging flags.
+# Should be invoked prior to any AC_PROG_* compiler checks.
+#
+# IS-RELEASE can be used to change the default to 'no' when making a
+# release. Set IS-RELEASE to 'yes' or 'no' as appropriate. By default, it
+# uses the value of $ax_is_release, so if you are using the AX_IS_RELEASE
+# macro, there is no need to pass this parameter.
+#
+# AX_IS_RELEASE([git-directory])
+# AX_CHECK_ENABLE_DEBUG()
+#
+# LICENSE
+#
+# Copyright (c) 2011 Rhys Ulerich <rhys.ulerich@gmail.com>
+# Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved.
+
+#serial 9
+
+AC_DEFUN([AX_CHECK_ENABLE_DEBUG],[
+ AC_BEFORE([$0],[AC_PROG_CC])dnl
+ AC_BEFORE([$0],[AC_PROG_CXX])dnl
+ AC_BEFORE([$0],[AC_PROG_F77])dnl
+ AC_BEFORE([$0],[AC_PROG_FC])dnl
+
+ AC_MSG_CHECKING(whether to enable debugging)
+
+ ax_enable_debug_default=m4_tolower(m4_normalize(ifelse([$1],,[no],[$1])))
+ ax_enable_debug_is_release=m4_tolower(m4_normalize(ifelse([$4],,
+ [$ax_is_release],
+ [$4])))
+
+ # If this is a release, override the default.
+ AS_IF([test "$ax_enable_debug_is_release" = "yes"],
+ [ax_enable_debug_default="no"])
+
+ m4_define(ax_enable_debug_vars,[m4_normalize(ifelse([$2],,,[$2]))])
+ m4_define(ax_disable_debug_vars,[m4_normalize(ifelse([$3],,[NDEBUG],[$3]))])
+
+ AC_ARG_ENABLE(debug,
+ [AS_HELP_STRING([--enable-debug=]@<:@yes/info/profile/no@:>@,[compile with debugging])],
+ [],enable_debug=$ax_enable_debug_default)
+
+ # empty mean debug yes
+ AS_IF([test "x$enable_debug" = "x"],
+ [enable_debug="yes"])
+
+ # case of debug
+ AS_CASE([$enable_debug],
+ [yes],[
+ AC_MSG_RESULT(yes)
+ CFLAGS="${CFLAGS} -g -O0"
+ CXXFLAGS="${CXXFLAGS} -g -O0"
+ FFLAGS="${FFLAGS} -g -O0"
+ FCFLAGS="${FCFLAGS} -g -O0"
+ OBJCFLAGS="${OBJCFLAGS} -g -O0"
+ ],
+ [info],[
+ AC_MSG_RESULT(info)
+ CFLAGS="${CFLAGS} -g"
+ CXXFLAGS="${CXXFLAGS} -g"
+ FFLAGS="${FFLAGS} -g"
+ FCFLAGS="${FCFLAGS} -g"
+ OBJCFLAGS="${OBJCFLAGS} -g"
+ ],
+ [profile],[
+ AC_MSG_RESULT(profile)
+ CFLAGS="${CFLAGS} -g -pg"
+ CXXFLAGS="${CXXFLAGS} -g -pg"
+ FFLAGS="${FFLAGS} -g -pg"
+ FCFLAGS="${FCFLAGS} -g -pg"
+ OBJCFLAGS="${OBJCFLAGS} -g -pg"
+ LDFLAGS="${LDFLAGS} -pg"
+ ],
+ [
+ AC_MSG_RESULT(no)
+ dnl Ensure AC_PROG_CC/CXX/F77/FC/OBJC will not enable debug flags
+ dnl by setting any unset environment flag variables
+ AS_IF([test "x${CFLAGS+set}" != "xset"],
+ [CFLAGS=""])
+ AS_IF([test "x${CXXFLAGS+set}" != "xset"],
+ [CXXFLAGS=""])
+ AS_IF([test "x${FFLAGS+set}" != "xset"],
+ [FFLAGS=""])
+ AS_IF([test "x${FCFLAGS+set}" != "xset"],
+ [FCFLAGS=""])
+ AS_IF([test "x${OBJCFLAGS+set}" != "xset"],
+ [OBJCFLAGS=""])
+ ])
+
+ dnl Define various variables if debugging is disabled.
+ dnl assert.h is a NOP if NDEBUG is defined, so define it by default.
+ AS_IF([test "x$enable_debug" = "xyes"],
+ [m4_map_args_w(ax_enable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is enabled])])],
+ [m4_map_args_w(ax_disable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is disabled])])])
+ ax_enable_debug=$enable_debug
+])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_check_gnu_make.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CHECK_GNU_MAKE([run-if-true],[run-if-false])
+#
+# DESCRIPTION
+#
+# This macro searches for a GNU version of make. If a match is found:
+#
+# * The makefile variable `ifGNUmake' is set to the empty string, otherwise
+# it is set to "#". This is useful for including a special features in a
+# Makefile, which cannot be handled by other versions of make.
+# * The makefile variable `ifnGNUmake' is set to #, otherwise
+# it is set to the empty string. This is useful for including a special
+# features in a Makefile, which can be handled
+# by other versions of make or to specify else like clause.
+# * The variable `_cv_gnu_make_command` is set to the command to invoke
+# GNU make if it exists, the empty string otherwise.
+# * The variable `ax_cv_gnu_make_command` is set to the command to invoke
+# GNU make by copying `_cv_gnu_make_command`, otherwise it is unset.
+# * If GNU Make is found, its version is extracted from the output of
+# `make --version` as the last field of a record of space-separated
+# columns and saved into the variable `ax_check_gnu_make_version`.
+# * Additionally if GNU Make is found, run shell code run-if-true
+# else run shell code run-if-false.
+#
+# Here is an example of its use:
+#
+# Makefile.in might contain:
+#
+# # A failsafe way of putting a dependency rule into a makefile
+# $(DEPEND):
+# $(CC) -MM $(srcdir)/*.c > $(DEPEND)
+#
+# @ifGNUmake@ ifeq ($(DEPEND),$(wildcard $(DEPEND)))
+# @ifGNUmake@ include $(DEPEND)
+# @ifGNUmake@ else
+# fallback code
+# @ifGNUmake@ endif
+#
+# Then configure.in would normally contain:
+#
+# AX_CHECK_GNU_MAKE()
+# AC_OUTPUT(Makefile)
+#
+# Then perhaps to cause gnu make to override any other make, we could do
+# something like this (note that GNU make always looks for GNUmakefile
+# first):
+#
+# if ! test x$_cv_gnu_make_command = x ; then
+# mv Makefile GNUmakefile
+# echo .DEFAULT: > Makefile ;
+# echo \ $_cv_gnu_make_command \$@ >> Makefile;
+# fi
+#
+# Then, if any (well almost any) other make is called, and GNU make also
+# exists, then the other make wraps the GNU make.
+#
+# LICENSE
+#
+# Copyright (c) 2008 John Darrington <j.darrington@elvis.murdoch.edu.au>
+# Copyright (c) 2015 Enrico M. Crisostomo <enrico.m.crisostomo@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 11
+
+AC_DEFUN([AX_CHECK_GNU_MAKE],dnl
+ [AC_PROG_AWK
+ AC_CACHE_CHECK([for GNU make],[_cv_gnu_make_command],[dnl
+ _cv_gnu_make_command="" ;
+dnl Search all the common names for GNU make
+ for a in "$MAKE" make gmake gnumake ; do
+ if test -z "$a" ; then continue ; fi ;
+ if "$a" --version 2> /dev/null | grep GNU 2>&1 > /dev/null ; then
+ _cv_gnu_make_command=$a ;
+ AX_CHECK_GNU_MAKE_HEADLINE=$("$a" --version 2> /dev/null | grep "GNU Make")
+ ax_check_gnu_make_version=$(echo ${AX_CHECK_GNU_MAKE_HEADLINE} | ${AWK} -F " " '{ print $(NF); }')
+ break ;
+ fi
+ done ;])
+dnl If there was a GNU version, then set @ifGNUmake@ to the empty string, '#' otherwise
+ AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifGNUmake], ["#"])], [AS_VAR_SET([ifGNUmake], [""])])
+ AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifnGNUmake], [""])], [AS_VAR_SET([ifGNUmake], ["#"])])
+ AS_VAR_IF([_cv_gnu_make_command], [""], [AS_UNSET(ax_cv_gnu_make_command)], [AS_VAR_SET([ax_cv_gnu_make_command], [${_cv_gnu_make_command}])])
+ AS_VAR_IF([_cv_gnu_make_command], [""],[$2],[$1])
+ AC_SUBST([ifGNUmake])
+ AC_SUBST([ifnGNUmake])
+])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_check_link_flag.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CHECK_LINK_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+# Check whether the given FLAG works with the linker or gives an error.
+# (Warnings, however, are ignored)
+#
+# ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on
+# success/failure.
+#
+# If EXTRA-FLAGS is defined, it is added to the linker's default flags
+# when the check is done. The check is thus made with the flags: "LDFLAGS
+# EXTRA-FLAGS FLAG". This can for example be used to force the linker to
+# issue an error when a bad flag is given.
+#
+# INPUT gives an alternative input source to AC_LINK_IFELSE.
+#
+# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this
+# macro in sync with AX_CHECK_{PREPROC,COMPILE}_FLAG.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
+# Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 6
+
+AC_DEFUN([AX_CHECK_LINK_FLAG],
+[AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF
+AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_ldflags_$4_$1])dnl
+AC_CACHE_CHECK([whether the linker accepts $1], CACHEVAR, [
+ ax_check_save_flags=$LDFLAGS
+ LDFLAGS="$LDFLAGS $4 $1"
+ AC_LINK_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])],
+ [AS_VAR_SET(CACHEVAR,[yes])],
+ [AS_VAR_SET(CACHEVAR,[no])])
+ LDFLAGS=$ax_check_save_flags])
+AS_VAR_IF(CACHEVAR,yes,
+ [m4_default([$2], :)],
+ [m4_default([$3], :)])
+AS_VAR_POPDEF([CACHEVAR])dnl
+])dnl AX_CHECK_LINK_FLAGS
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_code_coverage.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CODE_COVERAGE()
+#
+# DESCRIPTION
+#
+# Defines CODE_COVERAGE_CPPFLAGS, CODE_COVERAGE_CFLAGS,
+# CODE_COVERAGE_CXXFLAGS and CODE_COVERAGE_LIBS which should be included
+# in the CPPFLAGS, CFLAGS CXXFLAGS and LIBS/LIBADD variables of every
+# build target (program or library) which should be built with code
+# coverage support. Also add rules using AX_ADD_AM_MACRO_STATIC; and
+# $enable_code_coverage which can be used in subsequent configure output.
+# CODE_COVERAGE_ENABLED is defined and substituted, and corresponds to the
+# value of the --enable-code-coverage option, which defaults to being
+# disabled.
+#
+# Test also for gcov program and create GCOV variable that could be
+# substituted.
+#
+# Note that all optimization flags in CFLAGS must be disabled when code
+# coverage is enabled.
+#
+# Usage example:
+#
+# configure.ac:
+#
+# AX_CODE_COVERAGE
+#
+# Makefile.am:
+#
+# include $(top_srcdir)/aminclude_static.am
+#
+# my_program_LIBS = ... $(CODE_COVERAGE_LIBS) ...
+# my_program_CPPFLAGS = ... $(CODE_COVERAGE_CPPFLAGS) ...
+# my_program_CFLAGS = ... $(CODE_COVERAGE_CFLAGS) ...
+# my_program_CXXFLAGS = ... $(CODE_COVERAGE_CXXFLAGS) ...
+#
+# clean-local: code-coverage-clean
+# distclean-local: code-coverage-dist-clean
+#
+# This results in a "check-code-coverage" rule being added to any
+# Makefile.am which do "include $(top_srcdir)/aminclude_static.am"
+# (assuming the module has been configured with --enable-code-coverage).
+# Running `make check-code-coverage` in that directory will run the
+# module's test suite (`make check`) and build a code coverage report
+# detailing the code which was touched, then print the URI for the report.
+#
+# This code was derived from Makefile.decl in GLib, originally licensed
+# under LGPLv2.1+.
+#
+# LICENSE
+#
+# Copyright (c) 2012, 2016 Philip Withnall
+# Copyright (c) 2012 Xan Lopez
+# Copyright (c) 2012 Christian Persch
+# Copyright (c) 2012 Paolo Borelli
+# Copyright (c) 2012 Dan Winship
+# Copyright (c) 2015,2018 Bastien ROUCARIES
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or (at
+# your option) any later version.
+#
+# This library is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+#serial 33
+
+m4_define(_AX_CODE_COVERAGE_RULES,[
+AX_ADD_AM_MACRO_STATIC([
+# Code coverage
+#
+# Optional:
+# - CODE_COVERAGE_DIRECTORY: Top-level directory for code coverage reporting.
+# Multiple directories may be specified, separated by whitespace.
+# (Default: \$(top_builddir))
+# - CODE_COVERAGE_OUTPUT_FILE: Filename and path for the .info file generated
+# by lcov for code coverage. (Default:
+# \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info)
+# - CODE_COVERAGE_OUTPUT_DIRECTORY: Directory for generated code coverage
+# reports to be created. (Default:
+# \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage)
+# - CODE_COVERAGE_BRANCH_COVERAGE: Set to 1 to enforce branch coverage,
+# set to 0 to disable it and leave empty to stay with the default.
+# (Default: empty)
+# - CODE_COVERAGE_LCOV_SHOPTS_DEFAULT: Extra options shared between both lcov
+# instances. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE)
+# - CODE_COVERAGE_LCOV_SHOPTS: Extra options to shared between both lcov
+# instances. (Default: $CODE_COVERAGE_LCOV_SHOPTS_DEFAULT)
+# - CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH: --gcov-tool pathtogcov
+# - CODE_COVERAGE_LCOV_OPTIONS_DEFAULT: Extra options to pass to the
+# collecting lcov instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH)
+# - CODE_COVERAGE_LCOV_OPTIONS: Extra options to pass to the collecting lcov
+# instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_DEFAULT)
+# - CODE_COVERAGE_LCOV_RMOPTS_DEFAULT: Extra options to pass to the filtering
+# lcov instance. (Default: empty)
+# - CODE_COVERAGE_LCOV_RMOPTS: Extra options to pass to the filtering lcov
+# instance. (Default: $CODE_COVERAGE_LCOV_RMOPTS_DEFAULT)
+# - CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT: Extra options to pass to the
+# genhtml instance. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE)
+# - CODE_COVERAGE_GENHTML_OPTIONS: Extra options to pass to the genhtml
+# instance. (Default: $CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT)
+# - CODE_COVERAGE_IGNORE_PATTERN: Extra glob pattern of files to ignore
+#
+# The generated report will be titled using the \$(PACKAGE_NAME) and
+# \$(PACKAGE_VERSION). In order to add the current git hash to the title,
+# use the git-version-gen script, available online.
+# Optional variables
+# run only on top dir
+if CODE_COVERAGE_ENABLED
+ ifeq (\$(abs_builddir), \$(abs_top_builddir))
+CODE_COVERAGE_DIRECTORY ?= \$(top_builddir)
+CODE_COVERAGE_OUTPUT_FILE ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info
+CODE_COVERAGE_OUTPUT_DIRECTORY ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage
+
+CODE_COVERAGE_BRANCH_COVERAGE ?=
+CODE_COVERAGE_LCOV_SHOPTS_DEFAULT ?= \$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\
+--rc lcov_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE))
+CODE_COVERAGE_LCOV_SHOPTS ?= \$(CODE_COVERAGE_LCOV_SHOPTS_DEFAULT)
+CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH ?= --gcov-tool \"\$(GCOV)\"
+CODE_COVERAGE_LCOV_OPTIONS_DEFAULT ?= \$(CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH)
+CODE_COVERAGE_LCOV_OPTIONS ?= \$(CODE_COVERAGE_LCOV_OPTIONS_DEFAULT)
+CODE_COVERAGE_LCOV_RMOPTS_DEFAULT ?=
+CODE_COVERAGE_LCOV_RMOPTS ?= \$(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT)
+CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT ?=\
+\$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\
+--rc genhtml_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE))
+CODE_COVERAGE_GENHTML_OPTIONS ?= \$(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT)
+CODE_COVERAGE_IGNORE_PATTERN ?=
+
+GITIGNOREFILES = \$(GITIGNOREFILES) \$(CODE_COVERAGE_OUTPUT_FILE) \$(CODE_COVERAGE_OUTPUT_DIRECTORY)
+code_coverage_v_lcov_cap = \$(code_coverage_v_lcov_cap_\$(V))
+code_coverage_v_lcov_cap_ = \$(code_coverage_v_lcov_cap_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_lcov_cap_0 = @echo \" LCOV --capture\" \$(CODE_COVERAGE_OUTPUT_FILE);
+code_coverage_v_lcov_ign = \$(code_coverage_v_lcov_ign_\$(V))
+code_coverage_v_lcov_ign_ = \$(code_coverage_v_lcov_ign_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_lcov_ign_0 = @echo \" LCOV --remove /tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN);
+code_coverage_v_genhtml = \$(code_coverage_v_genhtml_\$(V))
+code_coverage_v_genhtml_ = \$(code_coverage_v_genhtml_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_genhtml_0 = @echo \" GEN \" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\";
+code_coverage_quiet = \$(code_coverage_quiet_\$(V))
+code_coverage_quiet_ = \$(code_coverage_quiet_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_quiet_0 = --quiet
+
+# sanitizes the test-name: replaces with underscores: dashes and dots
+code_coverage_sanitize = \$(subst -,_,\$(subst .,_,\$(1)))
+
+# Use recursive makes in order to ignore errors during check
+check-code-coverage:
+ -\$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) -k check
+ \$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) code-coverage-capture
+
+# Capture code coverage data
+code-coverage-capture: code-coverage-capture-hook
+ \$(code_coverage_v_lcov_cap)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --capture --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" --test-name \"\$(call code_coverage_sanitize,\$(PACKAGE_NAME)-\$(PACKAGE_VERSION))\" --no-checksum --compat-libtool \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_OPTIONS)
+ \$(code_coverage_v_lcov_ign)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --remove \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"/tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN) --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_RMOPTS)
+ -@rm -f \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\"
+ \$(code_coverage_v_genhtml)LANG=C \$(GENHTML) \$(code_coverage_quiet) \$(addprefix --prefix ,\$(CODE_COVERAGE_DIRECTORY)) --output-directory \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\" --title \"\$(PACKAGE_NAME)-\$(PACKAGE_VERSION) Code Coverage\" --legend --show-details \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_GENHTML_OPTIONS)
+ @echo \"file://\$(abs_builddir)/\$(CODE_COVERAGE_OUTPUT_DIRECTORY)/index.html\"
+
+code-coverage-clean:
+ -\$(LCOV) --directory \$(top_builddir) -z
+ -rm -rf \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\"
+ -find . \\( -name \"*.gcda\" -o -name \"*.gcno\" -o -name \"*.gcov\" \\) -delete
+
+code-coverage-dist-clean:
+
+A][M_DISTCHECK_CONFIGURE_FLAGS := \$(A][M_DISTCHECK_CONFIGURE_FLAGS) --disable-code-coverage
+ else # ifneq (\$(abs_builddir), \$(abs_top_builddir))
+check-code-coverage:
+
+code-coverage-capture: code-coverage-capture-hook
+
+code-coverage-clean:
+
+code-coverage-dist-clean:
+ endif # ifeq (\$(abs_builddir), \$(abs_top_builddir))
+else #! CODE_COVERAGE_ENABLED
+# Use recursive makes in order to ignore errors during check
+check-code-coverage:
+ @echo \"Need to reconfigure with --enable-code-coverage\"
+# Capture code coverage data
+code-coverage-capture: code-coverage-capture-hook
+ @echo \"Need to reconfigure with --enable-code-coverage\"
+
+code-coverage-clean:
+
+code-coverage-dist-clean:
+
+endif #CODE_COVERAGE_ENABLED
+# Hook rule executed before code-coverage-capture, overridable by the user
+code-coverage-capture-hook:
+
+.PHONY: check-code-coverage code-coverage-capture code-coverage-dist-clean code-coverage-clean code-coverage-capture-hook
+])
+])
+
+AC_DEFUN([_AX_CODE_COVERAGE_ENABLED],[
+ AX_CHECK_GNU_MAKE([],[AC_MSG_ERROR([not using GNU make that is needed for coverage])])
+ AC_REQUIRE([AX_ADD_AM_MACRO_STATIC])
+ # check for gcov
+ AC_CHECK_TOOL([GCOV],
+ [$_AX_CODE_COVERAGE_GCOV_PROG_WITH],
+ [:])
+ AS_IF([test "X$GCOV" = "X:"],
+ [AC_MSG_ERROR([gcov is needed to do coverage])])
+ AC_SUBST([GCOV])
+
+ dnl Check if gcc is being used
+ AS_IF([ test "$GCC" = "no" ], [
+ AC_MSG_ERROR([not compiling with gcc, which is required for gcov code coverage])
+ ])
+
+ AC_CHECK_PROG([LCOV], [lcov], [lcov])
+ AC_CHECK_PROG([GENHTML], [genhtml], [genhtml])
+
+ AS_IF([ test x"$LCOV" = x ], [
+ AC_MSG_ERROR([To enable code coverage reporting you must have lcov installed])
+ ])
+
+ AS_IF([ test x"$GENHTML" = x ], [
+ AC_MSG_ERROR([Could not find genhtml from the lcov package])
+ ])
+
+ dnl Build the code coverage flags
+ dnl Define CODE_COVERAGE_LDFLAGS for backwards compatibility
+ CODE_COVERAGE_CPPFLAGS="-DNDEBUG"
+ CODE_COVERAGE_CFLAGS="-O0 -g -fprofile-arcs -ftest-coverage"
+ CODE_COVERAGE_CXXFLAGS="-O0 -g -fprofile-arcs -ftest-coverage"
+ CODE_COVERAGE_LIBS="-lgcov"
+
+ AC_SUBST([CODE_COVERAGE_CPPFLAGS])
+ AC_SUBST([CODE_COVERAGE_CFLAGS])
+ AC_SUBST([CODE_COVERAGE_CXXFLAGS])
+ AC_SUBST([CODE_COVERAGE_LIBS])
+])
+
+AC_DEFUN([AX_CODE_COVERAGE],[
+ dnl Check for --enable-code-coverage
+
+ # allow to override gcov location
+ AC_ARG_WITH([gcov],
+ [AS_HELP_STRING([--with-gcov[=GCOV]], [use given GCOV for coverage (GCOV=gcov).])],
+ [_AX_CODE_COVERAGE_GCOV_PROG_WITH=$with_gcov],
+ [_AX_CODE_COVERAGE_GCOV_PROG_WITH=gcov])
+
+ AC_MSG_CHECKING([whether to build with code coverage support])
+ AC_ARG_ENABLE([code-coverage],
+ AS_HELP_STRING([--enable-code-coverage],
+ [Whether to enable code coverage support]),,
+ enable_code_coverage=no)
+
+ AM_CONDITIONAL([CODE_COVERAGE_ENABLED], [test "x$enable_code_coverage" = xyes])
+ AC_SUBST([CODE_COVERAGE_ENABLED], [$enable_code_coverage])
+ AC_MSG_RESULT($enable_code_coverage)
+
+ AS_IF([ test "x$enable_code_coverage" = xyes ], [
+ _AX_CODE_COVERAGE_ENABLED
+ ])
+
+ _AX_CODE_COVERAGE_RULES
+])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_COMPILER_FLAGS([CFLAGS-VARIABLE], [LDFLAGS-VARIABLE], [IS-RELEASE], [EXTRA-BASE-CFLAGS], [EXTRA-YES-CFLAGS], [UNUSED], [UNUSED], [UNUSED], [EXTRA-BASE-LDFLAGS], [EXTRA-YES-LDFLAGS], [UNUSED], [UNUSED], [UNUSED])
+#
+# DESCRIPTION
+#
+# Check for the presence of an --enable-compile-warnings option to
+# configure, defaulting to "error" in normal operation, or "yes" if
+# IS-RELEASE is equal to "yes". Return the value in the variable
+# $ax_enable_compile_warnings.
+#
+# Depending on the value of --enable-compile-warnings, different compiler
+# warnings are checked to see if they work with the current compiler and,
+# if so, are appended to CFLAGS-VARIABLE and LDFLAGS-VARIABLE. This
+# allows a consistent set of baseline compiler warnings to be used across
+# a code base, irrespective of any warnings enabled locally by individual
+# developers. By standardising the warnings used by all developers of a
+# project, the project can commit to a zero-warnings policy, using -Werror
+# to prevent compilation if new warnings are introduced. This makes
+# catching bugs which are flagged by warnings a lot easier.
+#
+# By providing a consistent --enable-compile-warnings argument across all
+# projects using this macro, continuous integration systems can easily be
+# configured the same for all projects. Automated systems or build
+# systems aimed at beginners may want to pass the --disable-Werror
+# argument to unconditionally prevent warnings being fatal.
+#
+# --enable-compile-warnings can take the values:
+#
+# * no: Base compiler warnings only; not even -Wall.
+# * yes: The above, plus a broad range of useful warnings.
+# * error: The above, plus -Werror so that all warnings are fatal.
+# Use --disable-Werror to override this and disable fatal
+# warnings.
+#
+# The set of base and enabled flags can be augmented using the
+# EXTRA-*-CFLAGS and EXTRA-*-LDFLAGS variables, which are tested and
+# appended to the output variable if --enable-compile-warnings is not
+# "no". Flags should not be disabled using these arguments, as the entire
+# point of AX_COMPILER_FLAGS is to enforce a consistent set of useful
+# compiler warnings on code, using warnings which have been chosen for low
+# false positive rates. If a compiler emits false positives for a
+# warning, a #pragma should be used in the code to disable the warning
+# locally. See:
+#
+# https://gcc.gnu.org/onlinedocs/gcc-4.9.2/gcc/Diagnostic-Pragmas.html#Diagnostic-Pragmas
+#
+# The EXTRA-* variables should only be used to supply extra warning flags,
+# and not general purpose compiler flags, as they are controlled by
+# configure options such as --disable-Werror.
+#
+# IS-RELEASE can be used to disable -Werror when making a release, which
+# is useful for those hairy moments when you just want to get the release
+# done as quickly as possible. Set it to "yes" to disable -Werror. By
+# default, it uses the value of $ax_is_release, so if you are using the
+# AX_IS_RELEASE macro, there is no need to pass this parameter. For
+# example:
+#
+# AX_IS_RELEASE([git-directory])
+# AX_COMPILER_FLAGS()
+#
+# CFLAGS-VARIABLE defaults to WARN_CFLAGS, and LDFLAGS-VARIABLE defaults
+# to WARN_LDFLAGS. Both variables are AC_SUBST-ed by this macro, but must
+# be manually added to the CFLAGS and LDFLAGS variables for each target in
+# the code base.
+#
+# If C++ language support is enabled with AC_PROG_CXX, which must occur
+# before this macro in configure.ac, warning flags for the C++ compiler
+# are AC_SUBST-ed as WARN_CXXFLAGS, and must be manually added to the
+# CXXFLAGS variables for each target in the code base. EXTRA-*-CFLAGS can
+# be used to augment the base and enabled flags.
+#
+# Warning flags for g-ir-scanner (from GObject Introspection) are
+# AC_SUBST-ed as WARN_SCANNERFLAGS. This variable must be manually added
+# to the SCANNERFLAGS variable for each GIR target in the code base. If
+# extra g-ir-scanner flags need to be enabled, the AX_COMPILER_FLAGS_GIR
+# macro must be invoked manually.
+#
+# AX_COMPILER_FLAGS may add support for other tools in future, in addition
+# to the compiler and linker. No extra EXTRA-* variables will be added
+# for those tools, and all extra support will still use the single
+# --enable-compile-warnings configure option. For finer grained control
+# over the flags for individual tools, use AX_COMPILER_FLAGS_CFLAGS,
+# AX_COMPILER_FLAGS_LDFLAGS and AX_COMPILER_FLAGS_* for new tools.
+#
+# The UNUSED variables date from a previous version of this macro, and are
+# automatically appended to the preceding non-UNUSED variable. They should
+# be left empty in new uses of the macro.
+#
+# LICENSE
+#
+# Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+# Copyright (c) 2015 David King <amigadave@amigadave.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 14
+
+# _AX_COMPILER_FLAGS_LANG([LANGNAME])
+m4_defun([_AX_COMPILER_FLAGS_LANG],
+[m4_ifdef([_AX_COMPILER_FLAGS_LANG_]$1[_enabled], [],
+ [m4_define([_AX_COMPILER_FLAGS_LANG_]$1[_enabled], [])dnl
+ AX_REQUIRE_DEFINED([AX_COMPILER_FLAGS_]$1[FLAGS])])dnl
+])
+
+AC_DEFUN([AX_COMPILER_FLAGS],[
+ # C support is enabled by default.
+ _AX_COMPILER_FLAGS_LANG([C])
+ # Only enable C++ support if AC_PROG_CXX is called. The redefinition of
+ # AC_PROG_CXX is so that a fatal error is emitted if this macro is called
+ # before AC_PROG_CXX, which would otherwise cause no C++ warnings to be
+ # checked.
+ AC_PROVIDE_IFELSE([AC_PROG_CXX],
+ [_AX_COMPILER_FLAGS_LANG([CXX])],
+ [m4_define([AC_PROG_CXX], defn([AC_PROG_CXX])[_AX_COMPILER_FLAGS_LANG([CXX])])])
+ AX_REQUIRE_DEFINED([AX_COMPILER_FLAGS_LDFLAGS])
+
+ # Default value for IS-RELEASE is $ax_is_release
+ ax_compiler_flags_is_release=m4_tolower(m4_normalize(ifelse([$3],,
+ [$ax_is_release],
+ [$3])))
+
+ AC_ARG_ENABLE([compile-warnings],
+ AS_HELP_STRING([--enable-compile-warnings=@<:@no/yes/error@:>@],
+ [Enable compiler warnings and errors]),,
+ [AS_IF([test "$ax_compiler_flags_is_release" = "yes"],
+ [enable_compile_warnings="yes"],
+ [enable_compile_warnings="error"])])
+ AC_ARG_ENABLE([Werror],
+ AS_HELP_STRING([--disable-Werror],
+ [Unconditionally make all compiler warnings non-fatal]),,
+ [enable_Werror=maybe])
+
+ # Return the user's chosen warning level
+ AS_IF([test "$enable_Werror" = "no" -a \
+ "$enable_compile_warnings" = "error"],[
+ enable_compile_warnings="yes"
+ ])
+
+ ax_enable_compile_warnings=$enable_compile_warnings
+
+ AX_COMPILER_FLAGS_CFLAGS([$1],[$ax_compiler_flags_is_release],
+ [$4],[$5 $6 $7 $8])
+ m4_ifdef([_AX_COMPILER_FLAGS_LANG_CXX_enabled],
+ [AX_COMPILER_FLAGS_CXXFLAGS([WARN_CXXFLAGS],
+ [$ax_compiler_flags_is_release],
+ [$4],[$5 $6 $7 $8])])
+ AX_COMPILER_FLAGS_LDFLAGS([$2],[$ax_compiler_flags_is_release],
+ [$9],[$10 $11 $12 $13])
+ AX_COMPILER_FLAGS_GIR([WARN_SCANNERFLAGS],[$ax_compiler_flags_is_release])
+])dnl AX_COMPILER_FLAGS
--- /dev/null
+# =============================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_cflags.html
+# =============================================================================
+#
+# SYNOPSIS
+#
+# AX_COMPILER_FLAGS_CFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS])
+#
+# DESCRIPTION
+#
+# Add warning flags for the C compiler to VARIABLE, which defaults to
+# WARN_CFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be
+# manually added to the CFLAGS variable for each target in the code base.
+#
+# This macro depends on the environment set up by AX_COMPILER_FLAGS.
+# Specifically, it uses the value of $ax_enable_compile_warnings to decide
+# which flags to enable.
+#
+# LICENSE
+#
+# Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+# Copyright (c) 2017, 2018 Reini Urban <rurban@cpan.org>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 17
+
+AC_DEFUN([AX_COMPILER_FLAGS_CFLAGS],[
+ AC_REQUIRE([AC_PROG_SED])
+ AX_REQUIRE_DEFINED([AX_APPEND_COMPILE_FLAGS])
+ AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+ AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG])
+
+ # Variable names
+ m4_define([ax_warn_cflags_variable],
+ [m4_normalize(ifelse([$1],,[WARN_CFLAGS],[$1]))])
+
+ AC_LANG_PUSH([C])
+
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([
+ [#ifndef __cplusplus
+ #error "no C++"
+ #endif]])],
+ [ax_compiler_cxx=yes;],
+ [ax_compiler_cxx=no;])
+
+ # Always pass -Werror=unknown-warning-option to get Clang to fail on bad
+ # flags, otherwise they are always appended to the warn_cflags variable, and
+ # Clang warns on them for every compilation unit.
+ # If this is passed to GCC, it will explode, so the flag must be enabled
+ # conditionally.
+ AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[
+ ax_compiler_flags_test="-Werror=unknown-warning-option"
+ ],[
+ ax_compiler_flags_test=""
+ ])
+
+ # Check that -Wno-suggest-attribute=format is supported
+ AX_CHECK_COMPILE_FLAG([-Wno-suggest-attribute=format],[
+ ax_compiler_no_suggest_attribute_flags="-Wno-suggest-attribute=format"
+ ],[
+ ax_compiler_no_suggest_attribute_flags=""
+ ])
+
+ # Base flags
+ AX_APPEND_COMPILE_FLAGS([ dnl
+ -fno-strict-aliasing dnl
+ $3 dnl
+ ],ax_warn_cflags_variable,[$ax_compiler_flags_test])
+
+ AS_IF([test "$ax_enable_compile_warnings" != "no"],[
+ if test "$ax_compiler_cxx" = "no" ; then
+ # C-only flags. Warn in C++
+ AX_APPEND_COMPILE_FLAGS([ dnl
+ -Wnested-externs dnl
+ -Wmissing-prototypes dnl
+ -Wstrict-prototypes dnl
+ -Wdeclaration-after-statement dnl
+ -Wimplicit-function-declaration dnl
+ -Wold-style-definition dnl
+ -Wjump-misses-init dnl
+ ],ax_warn_cflags_variable,[$ax_compiler_flags_test])
+ fi
+
+ # "yes" flags
+ AX_APPEND_COMPILE_FLAGS([ dnl
+ -Wall dnl
+ -Wextra dnl
+ -Wundef dnl
+ -Wwrite-strings dnl
+ -Wpointer-arith dnl
+ -Wmissing-declarations dnl
+ -Wredundant-decls dnl
+ -Wno-unused-parameter dnl
+ -Wno-missing-field-initializers dnl
+ -Wformat=2 dnl
+ -Wcast-align dnl
+ -Wformat-nonliteral dnl
+ -Wformat-security dnl
+ -Wsign-compare dnl
+ -Wstrict-aliasing dnl
+ -Wshadow dnl
+ -Winline dnl
+ -Wpacked dnl
+ -Wmissing-format-attribute dnl
+ -Wmissing-noreturn dnl
+ -Winit-self dnl
+ -Wredundant-decls dnl
+ -Wmissing-include-dirs dnl
+ -Wunused-but-set-variable dnl
+ -Warray-bounds dnl
+ -Wreturn-type dnl
+ -Wswitch-enum dnl
+ -Wswitch-default dnl
+ -Wduplicated-cond dnl
+ -Wduplicated-branches dnl
+ -Wlogical-op dnl
+ -Wrestrict dnl
+ -Wnull-dereference dnl
+ -Wdouble-promotion dnl
+ $4 dnl
+ $5 dnl
+ $6 dnl
+ $7 dnl
+ ],ax_warn_cflags_variable,[$ax_compiler_flags_test])
+ ])
+ AS_IF([test "$ax_enable_compile_warnings" = "error"],[
+ # "error" flags; -Werror has to be appended unconditionally because
+ # it's not possible to test for
+ #
+ # suggest-attribute=format is disabled because it gives too many false
+ # positives
+ AX_APPEND_FLAG([-Werror],ax_warn_cflags_variable)
+
+ AX_APPEND_COMPILE_FLAGS([ dnl
+ [$ax_compiler_no_suggest_attribute_flags] dnl
+ ],ax_warn_cflags_variable,[$ax_compiler_flags_test])
+ ])
+
+ # In the flags below, when disabling specific flags, always add *both*
+ # -Wno-foo and -Wno-error=foo. This fixes the situation where (for example)
+ # we enable -Werror, disable a flag, and a build bot passes CFLAGS=-Wall,
+ # which effectively turns that flag back on again as an error.
+ for flag in $ax_warn_cflags_variable; do
+ AS_CASE([$flag],
+ [-Wno-*=*],[],
+ [-Wno-*],[
+ AX_APPEND_COMPILE_FLAGS([-Wno-error=$(AS_ECHO([$flag]) | $SED 's/^-Wno-//')],
+ ax_warn_cflags_variable,
+ [$ax_compiler_flags_test])
+ ])
+ done
+
+ AC_LANG_POP([C])
+
+ # Substitute the variables
+ AC_SUBST(ax_warn_cflags_variable)
+])dnl AX_COMPILER_FLAGS
--- /dev/null
+# ===============================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_cxxflags.html
+# ===============================================================================
+#
+# SYNOPSIS
+#
+# AX_COMPILER_FLAGS_CXXFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS])
+#
+# DESCRIPTION
+#
+# Add warning flags for the C++ compiler to VARIABLE, which defaults to
+# WARN_CXXFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be
+# manually added to the CXXFLAGS variable for each target in the code
+# base.
+#
+# This macro depends on the environment set up by AX_COMPILER_FLAGS.
+# Specifically, it uses the value of $ax_enable_compile_warnings to decide
+# which flags to enable.
+#
+# LICENSE
+#
+# Copyright (c) 2015 David King <amigadave@amigadave.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 10
+
+AC_DEFUN([AX_COMPILER_FLAGS_CXXFLAGS],[
+ AC_REQUIRE([AC_PROG_SED])
+ AX_REQUIRE_DEFINED([AX_APPEND_COMPILE_FLAGS])
+ AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+ AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG])
+
+ # Variable names
+ m4_define([ax_warn_cxxflags_variable],
+ [m4_normalize(ifelse([$1],,[WARN_CXXFLAGS],[$1]))])
+
+ AC_LANG_PUSH([C++])
+
+ # Always pass -Werror=unknown-warning-option to get Clang to fail on bad
+ # flags, otherwise they are always appended to the warn_cxxflags variable,
+ # and Clang warns on them for every compilation unit.
+ # If this is passed to GCC, it will explode, so the flag must be enabled
+ # conditionally.
+ AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[
+ ax_compiler_flags_test="-Werror=unknown-warning-option"
+ ],[
+ ax_compiler_flags_test=""
+ ])
+
+ # Check that -Wno-suggest-attribute=format is supported
+ AX_CHECK_COMPILE_FLAG([-Wno-suggest-attribute=format],[
+ ax_compiler_no_suggest_attribute_flags="-Wno-suggest-attribute=format"
+ ],[
+ ax_compiler_no_suggest_attribute_flags=""
+ ])
+
+ # Base flags
+ AX_APPEND_COMPILE_FLAGS([ dnl
+ -fno-strict-aliasing dnl
+ $3 dnl
+ ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test])
+
+ AS_IF([test "$ax_enable_compile_warnings" != "no"],[
+ # "yes" flags
+ AX_APPEND_COMPILE_FLAGS([ dnl
+ -Wall dnl
+ -Wextra dnl
+ -Wundef dnl
+ -Wwrite-strings dnl
+ -Wpointer-arith dnl
+ -Wmissing-declarations dnl
+ -Wredundant-decls dnl
+ -Wno-unused-parameter dnl
+ -Wno-missing-field-initializers dnl
+ -Wformat=2 dnl
+ -Wcast-align dnl
+ -Wformat-nonliteral dnl
+ -Wformat-security dnl
+ -Wsign-compare dnl
+ -Wstrict-aliasing dnl
+ -Wshadow dnl
+ -Winline dnl
+ -Wpacked dnl
+ -Wmissing-format-attribute dnl
+ -Wmissing-noreturn dnl
+ -Winit-self dnl
+ -Wredundant-decls dnl
+ -Wmissing-include-dirs dnl
+ -Wunused-but-set-variable dnl
+ -Warray-bounds dnl
+ -Wreturn-type dnl
+ -Wno-overloaded-virtual dnl
+ -Wswitch-enum dnl
+ -Wswitch-default dnl
+ $4 dnl
+ $5 dnl
+ $6 dnl
+ $7 dnl
+ ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test])
+ ])
+ AS_IF([test "$ax_enable_compile_warnings" = "error"],[
+ # "error" flags; -Werror has to be appended unconditionally because
+ # it's not possible to test for
+ #
+ # suggest-attribute=format is disabled because it gives too many false
+ # positives
+ AX_APPEND_FLAG([-Werror],ax_warn_cxxflags_variable)
+
+ AX_APPEND_COMPILE_FLAGS([ dnl
+ [$ax_compiler_no_suggest_attribute_flags] dnl
+ ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test])
+ ])
+
+ # In the flags below, when disabling specific flags, always add *both*
+ # -Wno-foo and -Wno-error=foo. This fixes the situation where (for example)
+ # we enable -Werror, disable a flag, and a build bot passes CXXFLAGS=-Wall,
+ # which effectively turns that flag back on again as an error.
+ for flag in $ax_warn_cxxflags_variable; do
+ AS_CASE([$flag],
+ [-Wno-*=*],[],
+ [-Wno-*],[
+ AX_APPEND_COMPILE_FLAGS([-Wno-error=$(AS_ECHO([$flag]) | $SED 's/^-Wno-//')],
+ ax_warn_cxxflags_variable,
+ [$ax_compiler_flags_test])
+ ])
+ done
+
+ AC_LANG_POP([C++])
+
+ # Substitute the variables
+ AC_SUBST(ax_warn_cxxflags_variable)
+])dnl AX_COMPILER_FLAGS_CXXFLAGS
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_gir.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_COMPILER_FLAGS_GIR([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS])
+#
+# DESCRIPTION
+#
+# Add warning flags for the g-ir-scanner (from GObject Introspection) to
+# VARIABLE, which defaults to WARN_SCANNERFLAGS. VARIABLE is AC_SUBST-ed
+# by this macro, but must be manually added to the SCANNERFLAGS variable
+# for each GIR target in the code base.
+#
+# This macro depends on the environment set up by AX_COMPILER_FLAGS.
+# Specifically, it uses the value of $ax_enable_compile_warnings to decide
+# which flags to enable.
+#
+# LICENSE
+#
+# Copyright (c) 2015 Philip Withnall <philip@tecnocode.co.uk>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 6
+
+AC_DEFUN([AX_COMPILER_FLAGS_GIR],[
+ AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+
+ # Variable names
+ m4_define([ax_warn_scannerflags_variable],
+ [m4_normalize(ifelse([$1],,[WARN_SCANNERFLAGS],[$1]))])
+
+ # Base flags
+ AX_APPEND_FLAG([$3],ax_warn_scannerflags_variable)
+
+ AS_IF([test "$ax_enable_compile_warnings" != "no"],[
+ # "yes" flags
+ AX_APPEND_FLAG([ dnl
+ --warn-all dnl
+ $4 dnl
+ $5 dnl
+ $6 dnl
+ $7 dnl
+ ],ax_warn_scannerflags_variable)
+ ])
+ AS_IF([test "$ax_enable_compile_warnings" = "error"],[
+ # "error" flags
+ AX_APPEND_FLAG([ dnl
+ --warn-error dnl
+ ],ax_warn_scannerflags_variable)
+ ])
+
+ # Substitute the variables
+ AC_SUBST(ax_warn_scannerflags_variable)
+])dnl AX_COMPILER_FLAGS
--- /dev/null
+# ==============================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_ldflags.html
+# ==============================================================================
+#
+# SYNOPSIS
+#
+# AX_COMPILER_FLAGS_LDFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS])
+#
+# DESCRIPTION
+#
+# Add warning flags for the linker to VARIABLE, which defaults to
+# WARN_LDFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be
+# manually added to the LDFLAGS variable for each target in the code base.
+#
+# This macro depends on the environment set up by AX_COMPILER_FLAGS.
+# Specifically, it uses the value of $ax_enable_compile_warnings to decide
+# which flags to enable.
+#
+# LICENSE
+#
+# Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+# Copyright (c) 2017, 2018 Reini Urban <rurban@cpan.org>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 9
+
+AC_DEFUN([AX_COMPILER_FLAGS_LDFLAGS],[
+ AX_REQUIRE_DEFINED([AX_APPEND_LINK_FLAGS])
+ AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+ AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG])
+ AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG])
+
+ # Variable names
+ m4_define([ax_warn_ldflags_variable],
+ [m4_normalize(ifelse([$1],,[WARN_LDFLAGS],[$1]))])
+
+ # Always pass -Werror=unknown-warning-option to get Clang to fail on bad
+ # flags, otherwise they are always appended to the warn_ldflags variable,
+ # and Clang warns on them for every compilation unit.
+ # If this is passed to GCC, it will explode, so the flag must be enabled
+ # conditionally.
+ AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[
+ ax_compiler_flags_test="-Werror=unknown-warning-option"
+ ],[
+ ax_compiler_flags_test=""
+ ])
+
+ AX_CHECK_LINK_FLAG([-Wl,--as-needed], [
+ AX_APPEND_LINK_FLAGS([-Wl,--as-needed],
+ [AM_LDFLAGS],[$ax_compiler_flags_test])
+ ])
+ AX_CHECK_LINK_FLAG([-Wl,-z,relro], [
+ AX_APPEND_LINK_FLAGS([-Wl,-z,relro],
+ [AM_LDFLAGS],[$ax_compiler_flags_test])
+ ])
+ AX_CHECK_LINK_FLAG([-Wl,-z,now], [
+ AX_APPEND_LINK_FLAGS([-Wl,-z,now],
+ [AM_LDFLAGS],[$ax_compiler_flags_test])
+ ])
+ AX_CHECK_LINK_FLAG([-Wl,-z,noexecstack], [
+ AX_APPEND_LINK_FLAGS([-Wl,-z,noexecstack],
+ [AM_LDFLAGS],[$ax_compiler_flags_test])
+ ])
+ # textonly, retpolineplt not yet
+
+ # macOS and cygwin linker do not have --as-needed
+ AX_CHECK_LINK_FLAG([-Wl,--no-as-needed], [
+ ax_compiler_flags_as_needed_option="-Wl,--no-as-needed"
+ ], [
+ ax_compiler_flags_as_needed_option=""
+ ])
+
+ # macOS linker speaks with a different accent
+ ax_compiler_flags_fatal_warnings_option=""
+ AX_CHECK_LINK_FLAG([-Wl,--fatal-warnings], [
+ ax_compiler_flags_fatal_warnings_option="-Wl,--fatal-warnings"
+ ])
+ AX_CHECK_LINK_FLAG([-Wl,-fatal_warnings], [
+ ax_compiler_flags_fatal_warnings_option="-Wl,-fatal_warnings"
+ ])
+
+ # Base flags
+ AX_APPEND_LINK_FLAGS([ dnl
+ $ax_compiler_flags_as_needed_option dnl
+ $3 dnl
+ ],ax_warn_ldflags_variable,[$ax_compiler_flags_test])
+
+ AS_IF([test "$ax_enable_compile_warnings" != "no"],[
+ # "yes" flags
+ AX_APPEND_LINK_FLAGS([$4 $5 $6 $7],
+ ax_warn_ldflags_variable,
+ [$ax_compiler_flags_test])
+ ])
+ AS_IF([test "$ax_enable_compile_warnings" = "error"],[
+ # "error" flags; -Werror has to be appended unconditionally because
+ # it's not possible to test for
+ #
+ # suggest-attribute=format is disabled because it gives too many false
+ # positives
+ AX_APPEND_LINK_FLAGS([ dnl
+ $ax_compiler_flags_fatal_warnings_option dnl
+ ],ax_warn_ldflags_variable,[$ax_compiler_flags_test])
+ ])
+
+ # Substitute the variables
+ AC_SUBST(ax_warn_ldflags_variable)
+])dnl AX_COMPILER_FLAGS
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+# Check for baseline language coverage in the compiler for the specified
+# version of the C++ standard. If necessary, add switches to CXX and
+# CXXCPP to enable support. VERSION may be '11' (for the C++11 standard)
+# or '14' (for the C++14 standard).
+#
+# The second argument, if specified, indicates whether you insist on an
+# extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g.
+# -std=c++11). If neither is specified, you get whatever works, with
+# preference for an extended mode.
+#
+# The third argument, if specified 'mandatory' or if left unspecified,
+# indicates that baseline support for the specified C++ standard is
+# required and that the macro should error out if no mode with that
+# support is found. If specified 'optional', then configuration proceeds
+# regardless, after defining HAVE_CXX${VERSION} if and only if a
+# supporting mode is found.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Benjamin Kosnik <bkoz@redhat.com>
+# Copyright (c) 2012 Zack Weinberg <zackw@panix.com>
+# Copyright (c) 2013 Roy Stogner <roystgnr@ices.utexas.edu>
+# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov <sokolov@google.com>
+# Copyright (c) 2015 Paul Norman <penorman@mac.com>
+# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+# Copyright (c) 2016, 2018 Krzesimir Nowak <qdlacz@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 10
+
+dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro
+dnl (serial version number 13).
+
+AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl
+ m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"],
+ [$1], [14], [ax_cxx_compile_alternatives="14 1y"],
+ [$1], [17], [ax_cxx_compile_alternatives="17 1z"],
+ [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl
+ m4_if([$2], [], [],
+ [$2], [ext], [],
+ [$2], [noext], [],
+ [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl
+ m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true],
+ [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true],
+ [$3], [optional], [ax_cxx_compile_cxx$1_required=false],
+ [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])])
+ AC_LANG_PUSH([C++])dnl
+ ac_success=no
+
+ m4_if([$2], [noext], [], [dnl
+ if test x$ac_success = xno; then
+ for alternative in ${ax_cxx_compile_alternatives}; do
+ switch="-std=gnu++${alternative}"
+ cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+ $cachevar,
+ [ac_save_CXX="$CXX"
+ CXX="$CXX $switch"
+ AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [eval $cachevar=yes],
+ [eval $cachevar=no])
+ CXX="$ac_save_CXX"])
+ if eval test x\$$cachevar = xyes; then
+ CXX="$CXX $switch"
+ if test -n "$CXXCPP" ; then
+ CXXCPP="$CXXCPP $switch"
+ fi
+ ac_success=yes
+ break
+ fi
+ done
+ fi])
+
+ m4_if([$2], [ext], [], [dnl
+ if test x$ac_success = xno; then
+ dnl HP's aCC needs +std=c++11 according to:
+ dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf
+ dnl Cray's crayCC needs "-h std=c++11"
+ for alternative in ${ax_cxx_compile_alternatives}; do
+ for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do
+ cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+ $cachevar,
+ [ac_save_CXX="$CXX"
+ CXX="$CXX $switch"
+ AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [eval $cachevar=yes],
+ [eval $cachevar=no])
+ CXX="$ac_save_CXX"])
+ if eval test x\$$cachevar = xyes; then
+ CXX="$CXX $switch"
+ if test -n "$CXXCPP" ; then
+ CXXCPP="$CXXCPP $switch"
+ fi
+ ac_success=yes
+ break
+ fi
+ done
+ if test x$ac_success = xyes; then
+ break
+ fi
+ done
+ fi])
+ AC_LANG_POP([C++])
+ if test x$ax_cxx_compile_cxx$1_required = xtrue; then
+ if test x$ac_success = xno; then
+ AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.])
+ fi
+ fi
+ if test x$ac_success = xno; then
+ HAVE_CXX$1=0
+ AC_MSG_NOTICE([No compiler with C++$1 support was found])
+ else
+ HAVE_CXX$1=1
+ AC_DEFINE(HAVE_CXX$1,1,
+ [define if the compiler supports basic C++$1 syntax])
+ fi
+ AC_SUBST(HAVE_CXX$1)
+])
+
+
+dnl Test body for checking C++11 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+)
+
+
+dnl Test body for checking C++14 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+)
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_17
+)
+
+dnl Tests for new features in C++11
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[
+
+// If the compiler admits that it is not ready for C++11, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201103L
+
+#error "This is not a C++11 compiler"
+
+#else
+
+namespace cxx11
+{
+
+ namespace test_static_assert
+ {
+
+ template <typename T>
+ struct check
+ {
+ static_assert(sizeof(int) <= sizeof(T), "not big enough");
+ };
+
+ }
+
+ namespace test_final_override
+ {
+
+ struct Base
+ {
+ virtual void f() {}
+ };
+
+ struct Derived : public Base
+ {
+ virtual void f() override {}
+ };
+
+ }
+
+ namespace test_double_right_angle_brackets
+ {
+
+ template < typename T >
+ struct check {};
+
+ typedef check<void> single_type;
+ typedef check<check<void>> double_type;
+ typedef check<check<check<void>>> triple_type;
+ typedef check<check<check<check<void>>>> quadruple_type;
+
+ }
+
+ namespace test_decltype
+ {
+
+ int
+ f()
+ {
+ int a = 1;
+ decltype(a) b = 2;
+ return a + b;
+ }
+
+ }
+
+ namespace test_type_deduction
+ {
+
+ template < typename T1, typename T2 >
+ struct is_same
+ {
+ static const bool value = false;
+ };
+
+ template < typename T >
+ struct is_same<T, T>
+ {
+ static const bool value = true;
+ };
+
+ template < typename T1, typename T2 >
+ auto
+ add(T1 a1, T2 a2) -> decltype(a1 + a2)
+ {
+ return a1 + a2;
+ }
+
+ int
+ test(const int c, volatile int v)
+ {
+ static_assert(is_same<int, decltype(0)>::value == true, "");
+ static_assert(is_same<int, decltype(c)>::value == false, "");
+ static_assert(is_same<int, decltype(v)>::value == false, "");
+ auto ac = c;
+ auto av = v;
+ auto sumi = ac + av + 'x';
+ auto sumf = ac + av + 1.0;
+ static_assert(is_same<int, decltype(ac)>::value == true, "");
+ static_assert(is_same<int, decltype(av)>::value == true, "");
+ static_assert(is_same<int, decltype(sumi)>::value == true, "");
+ static_assert(is_same<int, decltype(sumf)>::value == false, "");
+ static_assert(is_same<int, decltype(add(c, v))>::value == true, "");
+ return (sumf > 0.0) ? sumi : add(c, v);
+ }
+
+ }
+
+ namespace test_noexcept
+ {
+
+ int f() { return 0; }
+ int g() noexcept { return 0; }
+
+ static_assert(noexcept(f()) == false, "");
+ static_assert(noexcept(g()) == true, "");
+
+ }
+
+ namespace test_constexpr
+ {
+
+ template < typename CharT >
+ unsigned long constexpr
+ strlen_c_r(const CharT *const s, const unsigned long acc) noexcept
+ {
+ return *s ? strlen_c_r(s + 1, acc + 1) : acc;
+ }
+
+ template < typename CharT >
+ unsigned long constexpr
+ strlen_c(const CharT *const s) noexcept
+ {
+ return strlen_c_r(s, 0UL);
+ }
+
+ static_assert(strlen_c("") == 0UL, "");
+ static_assert(strlen_c("1") == 1UL, "");
+ static_assert(strlen_c("example") == 7UL, "");
+ static_assert(strlen_c("another\0example") == 7UL, "");
+
+ }
+
+ namespace test_rvalue_references
+ {
+
+ template < int N >
+ struct answer
+ {
+ static constexpr int value = N;
+ };
+
+ answer<1> f(int&) { return answer<1>(); }
+ answer<2> f(const int&) { return answer<2>(); }
+ answer<3> f(int&&) { return answer<3>(); }
+
+ void
+ test()
+ {
+ int i = 0;
+ const int c = 0;
+ static_assert(decltype(f(i))::value == 1, "");
+ static_assert(decltype(f(c))::value == 2, "");
+ static_assert(decltype(f(0))::value == 3, "");
+ }
+
+ }
+
+ namespace test_uniform_initialization
+ {
+
+ struct test
+ {
+ static const int zero {};
+ static const int one {1};
+ };
+
+ static_assert(test::zero == 0, "");
+ static_assert(test::one == 1, "");
+
+ }
+
+ namespace test_lambdas
+ {
+
+ void
+ test1()
+ {
+ auto lambda1 = [](){};
+ auto lambda2 = lambda1;
+ lambda1();
+ lambda2();
+ }
+
+ int
+ test2()
+ {
+ auto a = [](int i, int j){ return i + j; }(1, 2);
+ auto b = []() -> int { return '0'; }();
+ auto c = [=](){ return a + b; }();
+ auto d = [&](){ return c; }();
+ auto e = [a, &b](int x) mutable {
+ const auto identity = [](int y){ return y; };
+ for (auto i = 0; i < a; ++i)
+ a += b--;
+ return x + identity(a + b);
+ }(0);
+ return a + b + c + d + e;
+ }
+
+ int
+ test3()
+ {
+ const auto nullary = [](){ return 0; };
+ const auto unary = [](int x){ return x; };
+ using nullary_t = decltype(nullary);
+ using unary_t = decltype(unary);
+ const auto higher1st = [](nullary_t f){ return f(); };
+ const auto higher2nd = [unary](nullary_t f1){
+ return [unary, f1](unary_t f2){ return f2(unary(f1())); };
+ };
+ return higher1st(nullary) + higher2nd(nullary)(unary);
+ }
+
+ }
+
+ namespace test_variadic_templates
+ {
+
+ template <int...>
+ struct sum;
+
+ template <int N0, int... N1toN>
+ struct sum<N0, N1toN...>
+ {
+ static constexpr auto value = N0 + sum<N1toN...>::value;
+ };
+
+ template <>
+ struct sum<>
+ {
+ static constexpr auto value = 0;
+ };
+
+ static_assert(sum<>::value == 0, "");
+ static_assert(sum<1>::value == 1, "");
+ static_assert(sum<23>::value == 23, "");
+ static_assert(sum<1, 2>::value == 3, "");
+ static_assert(sum<5, 5, 11>::value == 21, "");
+ static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, "");
+
+ }
+
+ // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae
+ // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function
+ // because of this.
+ namespace test_template_alias_sfinae
+ {
+
+ struct foo {};
+
+ template<typename T>
+ using member = typename T::member_type;
+
+ template<typename T>
+ void func(...) {}
+
+ template<typename T>
+ void func(member<T>*) {}
+
+ void test();
+
+ void test() { func<foo>(0); }
+
+ }
+
+} // namespace cxx11
+
+#endif // __cplusplus >= 201103L
+
+]])
+
+
+dnl Tests for new features in C++14
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[
+
+// If the compiler admits that it is not ready for C++14, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201402L
+
+#error "This is not a C++14 compiler"
+
+#else
+
+namespace cxx14
+{
+
+ namespace test_polymorphic_lambdas
+ {
+
+ int
+ test()
+ {
+ const auto lambda = [](auto&&... args){
+ const auto istiny = [](auto x){
+ return (sizeof(x) == 1UL) ? 1 : 0;
+ };
+ const int aretiny[] = { istiny(args)... };
+ return aretiny[0];
+ };
+ return lambda(1, 1L, 1.0f, '1');
+ }
+
+ }
+
+ namespace test_binary_literals
+ {
+
+ constexpr auto ivii = 0b0000000000101010;
+ static_assert(ivii == 42, "wrong value");
+
+ }
+
+ namespace test_generalized_constexpr
+ {
+
+ template < typename CharT >
+ constexpr unsigned long
+ strlen_c(const CharT *const s) noexcept
+ {
+ auto length = 0UL;
+ for (auto p = s; *p; ++p)
+ ++length;
+ return length;
+ }
+
+ static_assert(strlen_c("") == 0UL, "");
+ static_assert(strlen_c("x") == 1UL, "");
+ static_assert(strlen_c("test") == 4UL, "");
+ static_assert(strlen_c("another\0test") == 7UL, "");
+
+ }
+
+ namespace test_lambda_init_capture
+ {
+
+ int
+ test()
+ {
+ auto x = 0;
+ const auto lambda1 = [a = x](int b){ return a + b; };
+ const auto lambda2 = [a = lambda1(x)](){ return a; };
+ return lambda2();
+ }
+
+ }
+
+ namespace test_digit_separators
+ {
+
+ constexpr auto ten_million = 100'000'000;
+ static_assert(ten_million == 100000000, "");
+
+ }
+
+ namespace test_return_type_deduction
+ {
+
+ auto f(int& x) { return x; }
+ decltype(auto) g(int& x) { return x; }
+
+ template < typename T1, typename T2 >
+ struct is_same
+ {
+ static constexpr auto value = false;
+ };
+
+ template < typename T >
+ struct is_same<T, T>
+ {
+ static constexpr auto value = true;
+ };
+
+ int
+ test()
+ {
+ auto x = 0;
+ static_assert(is_same<int, decltype(f(x))>::value, "");
+ static_assert(is_same<int&, decltype(g(x))>::value, "");
+ return x;
+ }
+
+ }
+
+} // namespace cxx14
+
+#endif // __cplusplus >= 201402L
+
+]])
+
+
+dnl Tests for new features in C++17
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[
+
+// If the compiler admits that it is not ready for C++17, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201703L
+
+#error "This is not a C++17 compiler"
+
+#else
+
+#include <initializer_list>
+#include <utility>
+#include <type_traits>
+
+namespace cxx17
+{
+
+ namespace test_constexpr_lambdas
+ {
+
+ constexpr int foo = [](){return 42;}();
+
+ }
+
+ namespace test::nested_namespace::definitions
+ {
+
+ }
+
+ namespace test_fold_expression
+ {
+
+ template<typename... Args>
+ int multiply(Args... args)
+ {
+ return (args * ... * 1);
+ }
+
+ template<typename... Args>
+ bool all(Args... args)
+ {
+ return (args && ...);
+ }
+
+ }
+
+ namespace test_extended_static_assert
+ {
+
+ static_assert (true);
+
+ }
+
+ namespace test_auto_brace_init_list
+ {
+
+ auto foo = {5};
+ auto bar {5};
+
+ static_assert(std::is_same<std::initializer_list<int>, decltype(foo)>::value);
+ static_assert(std::is_same<int, decltype(bar)>::value);
+ }
+
+ namespace test_typename_in_template_template_parameter
+ {
+
+ template<template<typename> typename X> struct D;
+
+ }
+
+ namespace test_fallthrough_nodiscard_maybe_unused_attributes
+ {
+
+ int f1()
+ {
+ return 42;
+ }
+
+ [[nodiscard]] int f2()
+ {
+ [[maybe_unused]] auto unused = f1();
+
+ switch (f1())
+ {
+ case 17:
+ f1();
+ [[fallthrough]];
+ case 42:
+ f1();
+ }
+ return f1();
+ }
+
+ }
+
+ namespace test_extended_aggregate_initialization
+ {
+
+ struct base1
+ {
+ int b1, b2 = 42;
+ };
+
+ struct base2
+ {
+ base2() {
+ b3 = 42;
+ }
+ int b3;
+ };
+
+ struct derived : base1, base2
+ {
+ int d;
+ };
+
+ derived d1 {{1, 2}, {}, 4}; // full initialization
+ derived d2 {{}, {}, 4}; // value-initialized bases
+
+ }
+
+ namespace test_general_range_based_for_loop
+ {
+
+ struct iter
+ {
+ int i;
+
+ int& operator* ()
+ {
+ return i;
+ }
+
+ const int& operator* () const
+ {
+ return i;
+ }
+
+ iter& operator++()
+ {
+ ++i;
+ return *this;
+ }
+ };
+
+ struct sentinel
+ {
+ int i;
+ };
+
+ bool operator== (const iter& i, const sentinel& s)
+ {
+ return i.i == s.i;
+ }
+
+ bool operator!= (const iter& i, const sentinel& s)
+ {
+ return !(i == s);
+ }
+
+ struct range
+ {
+ iter begin() const
+ {
+ return {0};
+ }
+
+ sentinel end() const
+ {
+ return {5};
+ }
+ };
+
+ void f()
+ {
+ range r {};
+
+ for (auto i : r)
+ {
+ [[maybe_unused]] auto v = i;
+ }
+ }
+
+ }
+
+ namespace test_lambda_capture_asterisk_this_by_value
+ {
+
+ struct t
+ {
+ int i;
+ int foo()
+ {
+ return [*this]()
+ {
+ return i;
+ }();
+ }
+ };
+
+ }
+
+ namespace test_enum_class_construction
+ {
+
+ enum class byte : unsigned char
+ {};
+
+ byte foo {42};
+
+ }
+
+ namespace test_constexpr_if
+ {
+
+ template <bool cond>
+ int f ()
+ {
+ if constexpr(cond)
+ {
+ return 13;
+ }
+ else
+ {
+ return 42;
+ }
+ }
+
+ }
+
+ namespace test_selection_statement_with_initializer
+ {
+
+ int f()
+ {
+ return 13;
+ }
+
+ int f2()
+ {
+ if (auto i = f(); i > 0)
+ {
+ return 3;
+ }
+
+ switch (auto i = f(); i + 4)
+ {
+ case 17:
+ return 2;
+
+ default:
+ return 1;
+ }
+ }
+
+ }
+
+ namespace test_template_argument_deduction_for_class_templates
+ {
+
+ template <typename T1, typename T2>
+ struct pair
+ {
+ pair (T1 p1, T2 p2)
+ : m1 {p1},
+ m2 {p2}
+ {}
+
+ T1 m1;
+ T2 m2;
+ };
+
+ void f()
+ {
+ [[maybe_unused]] auto p = pair{13, 42u};
+ }
+
+ }
+
+ namespace test_non_type_auto_template_parameters
+ {
+
+ template <auto n>
+ struct B
+ {};
+
+ B<5> b1;
+ B<'a'> b2;
+
+ }
+
+ namespace test_structured_bindings
+ {
+
+ int arr[2] = { 1, 2 };
+ std::pair<int, int> pr = { 1, 2 };
+
+ auto f1() -> int(&)[2]
+ {
+ return arr;
+ }
+
+ auto f2() -> std::pair<int, int>&
+ {
+ return pr;
+ }
+
+ struct S
+ {
+ int x1 : 2;
+ volatile double y1;
+ };
+
+ S f3()
+ {
+ return {};
+ }
+
+ auto [ x1, y1 ] = f1();
+ auto& [ xr1, yr1 ] = f1();
+ auto [ x2, y2 ] = f2();
+ auto& [ xr2, yr2 ] = f2();
+ const auto [ x3, y3 ] = f3();
+
+ }
+
+ namespace test_exception_spec_type_system
+ {
+
+ struct Good {};
+ struct Bad {};
+
+ void g1() noexcept;
+ void g2();
+
+ template<typename T>
+ Bad
+ f(T*, T*);
+
+ template<typename T1, typename T2>
+ Good
+ f(T1*, T2*);
+
+ static_assert (std::is_same_v<Good, decltype(f(g1, g2))>);
+
+ }
+
+ namespace test_inline_variables
+ {
+
+ template<class T> void f(T)
+ {}
+
+ template<class T> inline T g(T)
+ {
+ return T{};
+ }
+
+ template<> inline void f<>(int)
+ {}
+
+ template<> int g<>(int)
+ {
+ return 5;
+ }
+
+ }
+
+} // namespace cxx17
+
+#endif // __cplusplus < 201703L
+
+]])
--- /dev/null
+# =============================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_17.html
+# =============================================================================
+#
+# SYNOPSIS
+#
+# AX_CXX_COMPILE_STDCXX_17([ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+# Check for baseline language coverage in the compiler for the C++17
+# standard; if necessary, add switches to CXX and CXXCPP to enable
+# support.
+#
+# This macro is a convenience alias for calling the AX_CXX_COMPILE_STDCXX
+# macro with the version set to C++17. The two optional arguments are
+# forwarded literally as the second and third argument respectively.
+# Please see the documentation for the AX_CXX_COMPILE_STDCXX macro for
+# more information. If you want to use this macro, you also need to
+# download the ax_cxx_compile_stdcxx.m4 file.
+#
+# LICENSE
+#
+# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+# Copyright (c) 2016 Krzesimir Nowak <qdlacz@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 2
+
+AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX])
+AC_DEFUN([AX_CXX_COMPILE_STDCXX_17], [AX_CXX_COMPILE_STDCXX([17], [$1], [$2])])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_file_escapes.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_FILE_ESCAPES
+#
+# DESCRIPTION
+#
+# Writes the specified data to the specified file.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Tom Howard <tomhoward@users.sf.net>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 8
+
+AC_DEFUN([AX_FILE_ESCAPES],[
+AX_DOLLAR="\$"
+AX_SRB="\\135"
+AX_SLB="\\133"
+AX_BS="\\\\"
+AX_DQ="\""
+])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_is_release.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_IS_RELEASE(POLICY)
+#
+# DESCRIPTION
+#
+# Determine whether the code is being configured as a release, or from
+# git. Set the ax_is_release variable to 'yes' or 'no'.
+#
+# If building a release version, it is recommended that the configure
+# script disable compiler errors and debug features, by conditionalising
+# them on the ax_is_release variable. If building from git, these
+# features should be enabled.
+#
+# The POLICY parameter specifies how ax_is_release is determined. It can
+# take the following values:
+#
+# * git-directory: ax_is_release will be 'no' if a '.git' directory exists
+# * minor-version: ax_is_release will be 'no' if the minor version number
+# in $PACKAGE_VERSION is odd; this assumes
+# $PACKAGE_VERSION follows the 'major.minor.micro' scheme
+# * micro-version: ax_is_release will be 'no' if the micro version number
+# in $PACKAGE_VERSION is odd; this assumes
+# $PACKAGE_VERSION follows the 'major.minor.micro' scheme
+# * dash-version: ax_is_release will be 'no' if there is a dash '-'
+# in $PACKAGE_VERSION, for example 1.2-pre3, 1.2.42-a8b9
+# or 2.0-dirty (in particular this is suitable for use
+# with git-version-gen)
+# * always: ax_is_release will always be 'yes'
+# * never: ax_is_release will always be 'no'
+#
+# Other policies may be added in future.
+#
+# LICENSE
+#
+# Copyright (c) 2015 Philip Withnall <philip@tecnocode.co.uk>
+# Copyright (c) 2016 Collabora Ltd.
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved.
+
+#serial 7
+
+AC_DEFUN([AX_IS_RELEASE],[
+ AC_BEFORE([AC_INIT],[$0])
+
+ m4_case([$1],
+ [git-directory],[
+ # $is_release = (.git directory does not exist)
+ AS_IF([test -d ${srcdir}/.git],[ax_is_release=no],[ax_is_release=yes])
+ ],
+ [minor-version],[
+ # $is_release = ($minor_version is even)
+ minor_version=`echo "$PACKAGE_VERSION" | sed 's/[[^.]][[^.]]*.\([[^.]][[^.]]*\).*/\1/'`
+ AS_IF([test "$(( $minor_version % 2 ))" -ne 0],
+ [ax_is_release=no],[ax_is_release=yes])
+ ],
+ [micro-version],[
+ # $is_release = ($micro_version is even)
+ micro_version=`echo "$PACKAGE_VERSION" | sed 's/[[^.]]*\.[[^.]]*\.\([[^.]]*\).*/\1/'`
+ AS_IF([test "$(( $micro_version % 2 ))" -ne 0],
+ [ax_is_release=no],[ax_is_release=yes])
+ ],
+ [dash-version],[
+ # $is_release = ($PACKAGE_VERSION has a dash)
+ AS_CASE([$PACKAGE_VERSION],
+ [*-*], [ax_is_release=no],
+ [*], [ax_is_release=yes])
+ ],
+ [always],[ax_is_release=yes],
+ [never],[ax_is_release=no],
+ [
+ AC_MSG_ERROR([Invalid policy. Valid policies: git-directory, minor-version, micro-version, dash-version, always, never.])
+ ])
+])
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_lib_readline.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_LIB_READLINE
+#
+# DESCRIPTION
+#
+# Searches for a readline compatible library. If found, defines
+# `HAVE_LIBREADLINE'. If the found library has the `add_history' function,
+# sets also `HAVE_READLINE_HISTORY'. Also checks for the locations of the
+# necessary include files and sets `HAVE_READLINE_H' or
+# `HAVE_READLINE_READLINE_H' and `HAVE_READLINE_HISTORY_H' or
+# 'HAVE_HISTORY_H' if the corresponding include files exists.
+#
+# The libraries that may be readline compatible are `libedit',
+# `libeditline' and `libreadline'. Sometimes we need to link a termcap
+# library for readline to work, this macro tests these cases too by trying
+# to link with `libtermcap', `libcurses' or `libncurses' before giving up.
+#
+# Here is an example of how to use the information provided by this macro
+# to perform the necessary includes or declarations in a C file:
+#
+# #ifdef HAVE_LIBREADLINE
+# # if defined(HAVE_READLINE_READLINE_H)
+# # include <readline/readline.h>
+# # elif defined(HAVE_READLINE_H)
+# # include <readline.h>
+# # else /* !defined(HAVE_READLINE_H) */
+# extern char *readline ();
+# # endif /* !defined(HAVE_READLINE_H) */
+# char *cmdline = NULL;
+# #else /* !defined(HAVE_READLINE_READLINE_H) */
+# /* no readline */
+# #endif /* HAVE_LIBREADLINE */
+#
+# #ifdef HAVE_READLINE_HISTORY
+# # if defined(HAVE_READLINE_HISTORY_H)
+# # include <readline/history.h>
+# # elif defined(HAVE_HISTORY_H)
+# # include <history.h>
+# # else /* !defined(HAVE_HISTORY_H) */
+# extern void add_history ();
+# extern int write_history ();
+# extern int read_history ();
+# # endif /* defined(HAVE_READLINE_HISTORY_H) */
+# /* no history */
+# #endif /* HAVE_READLINE_HISTORY */
+#
+# LICENSE
+#
+# Copyright (c) 2008 Ville Laurikari <vl@iki.fi>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 8
+
+AU_ALIAS([VL_LIB_READLINE], [AX_LIB_READLINE])
+AC_DEFUN([AX_LIB_READLINE], [
+ AC_CACHE_CHECK([for a readline compatible library],
+ ax_cv_lib_readline, [
+ ORIG_LIBS="$LIBS"
+ # djcb: we need the _real_ readline_
+ #for readline_lib in readline edit editline; do
+ for readline_lib in readline; do
+ for termcap_lib in "" termcap curses ncurses; do
+ if test -z "$termcap_lib"; then
+ TRY_LIB="-l$readline_lib"
+ else
+ TRY_LIB="-l$readline_lib -l$termcap_lib"
+ fi
+ LIBS="$ORIG_LIBS $TRY_LIB"
+ AC_LINK_IFELSE([AC_LANG_CALL([], [readline])], [ax_cv_lib_readline="$TRY_LIB"])
+ if test -n "$ax_cv_lib_readline"; then
+ break
+ fi
+ done
+ if test -n "$ax_cv_lib_readline"; then
+ break
+ fi
+ done
+ if test -z "$ax_cv_lib_readline"; then
+ ax_cv_lib_readline="no"
+ fi
+ LIBS="$ORIG_LIBS"
+ ])
+
+ if test "$ax_cv_lib_readline" != "no"; then
+ LIBS="$LIBS $ax_cv_lib_readline"
+ AC_DEFINE(HAVE_LIBREADLINE, 1,
+ [Define if you have a readline compatible library])
+ AC_CHECK_HEADERS(readline.h readline/readline.h)
+ AC_CACHE_CHECK([whether readline supports history],
+ ax_cv_lib_readline_history, [
+ ax_cv_lib_readline_history="no"
+ AC_LINK_IFELSE([AC_LANG_CALL([], [add_history])], [ax_cv_lib_readline_history="yes"])
+ ])
+ if test "$ax_cv_lib_readline_history" = "yes"; then
+ AC_DEFINE(HAVE_READLINE_HISTORY, 1,
+ [Define if your readline library has \`add_history'])
+ AC_CHECK_HEADERS(history.h readline/history.h)
+ fi
+ fi
+])dnl
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_require_defined.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_REQUIRE_DEFINED(MACRO)
+#
+# DESCRIPTION
+#
+# AX_REQUIRE_DEFINED is a simple helper for making sure other macros have
+# been defined and thus are available for use. This avoids random issues
+# where a macro isn't expanded. Instead the configure script emits a
+# non-fatal:
+#
+# ./configure: line 1673: AX_CFLAGS_WARN_ALL: command not found
+#
+# It's like AC_REQUIRE except it doesn't expand the required macro.
+#
+# Here's an example:
+#
+# AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG])
+#
+# LICENSE
+#
+# Copyright (c) 2014 Mike Frysinger <vapier@gentoo.org>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 2
+
+AC_DEFUN([AX_REQUIRE_DEFINED], [dnl
+ m4_ifndef([$1], [m4_fatal([macro ]$1[ is not defined; is a m4 file missing?])])
+])dnl AX_REQUIRE_DEFINED
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_valgrind_check.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_VALGRIND_DFLT(memcheck|helgrind|drd|sgcheck, on|off)
+# AX_VALGRIND_CHECK()
+#
+# DESCRIPTION
+#
+# AX_VALGRIND_CHECK checks whether Valgrind is present and, if so, allows
+# running `make check` under a variety of Valgrind tools to check for
+# memory and threading errors.
+#
+# Defines VALGRIND_CHECK_RULES which should be substituted in your
+# Makefile; and $enable_valgrind which can be used in subsequent configure
+# output. VALGRIND_ENABLED is defined and substituted, and corresponds to
+# the value of the --enable-valgrind option, which defaults to being
+# enabled if Valgrind is installed and disabled otherwise. Individual
+# Valgrind tools can be disabled via --disable-valgrind-<tool>, the
+# default is configurable via the AX_VALGRIND_DFLT command or is to use
+# all commands not disabled via AX_VALGRIND_DFLT. All AX_VALGRIND_DFLT
+# calls must be made before the call to AX_VALGRIND_CHECK.
+#
+# If unit tests are written using a shell script and automake's
+# LOG_COMPILER system, the $(VALGRIND) variable can be used within the
+# shell scripts to enable Valgrind, as described here:
+#
+# https://www.gnu.org/software/gnulib/manual/html_node/Running-self_002dtests-under-valgrind.html
+#
+# Usage example:
+#
+# configure.ac:
+#
+# AX_VALGRIND_DFLT([sgcheck], [off])
+# AX_VALGRIND_CHECK
+#
+# in each Makefile.am with tests:
+#
+# @VALGRIND_CHECK_RULES@
+# VALGRIND_SUPPRESSIONS_FILES = my-project.supp
+# EXTRA_DIST = my-project.supp
+#
+# This results in a "check-valgrind" rule being added. Running `make
+# check-valgrind` in that directory will recursively run the module's test
+# suite (`make check`) once for each of the available Valgrind tools (out
+# of memcheck, helgrind and drd) while the sgcheck will be skipped unless
+# enabled again on the commandline with --enable-valgrind-sgcheck. The
+# results for each check will be output to test-suite-$toolname.log. The
+# target will succeed if there are zero errors and fail otherwise.
+#
+# Alternatively, a "check-valgrind-$TOOL" rule will be added, for $TOOL in
+# memcheck, helgrind, drd and sgcheck. These are useful because often only
+# some of those tools can be ran cleanly on a codebase.
+#
+# The macro supports running with and without libtool.
+#
+# LICENSE
+#
+# Copyright (c) 2014, 2015, 2016 Philip Withnall <philip.withnall@collabora.co.uk>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 17
+
+dnl Configured tools
+m4_define([valgrind_tool_list], [[memcheck], [helgrind], [drd], [sgcheck]])
+m4_set_add_all([valgrind_exp_tool_set], [sgcheck])
+m4_foreach([vgtool], [valgrind_tool_list],
+ [m4_define([en_dflt_valgrind_]vgtool, [on])])
+
+AC_DEFUN([AX_VALGRIND_DFLT],[
+ m4_define([en_dflt_valgrind_$1], [$2])
+])dnl
+
+AM_EXTRA_RECURSIVE_TARGETS([check-valgrind])
+m4_foreach([vgtool], [valgrind_tool_list],
+ [AM_EXTRA_RECURSIVE_TARGETS([check-valgrind-]vgtool)])
+
+AC_DEFUN([AX_VALGRIND_CHECK],[
+ dnl Check for --enable-valgrind
+ AC_ARG_ENABLE([valgrind],
+ [AS_HELP_STRING([--enable-valgrind], [Whether to enable Valgrind on the unit tests])],
+ [enable_valgrind=$enableval],[enable_valgrind=])
+
+ AS_IF([test "$enable_valgrind" != "no"],[
+ # Check for Valgrind.
+ AC_CHECK_PROG([VALGRIND],[valgrind],[valgrind])
+ AS_IF([test "$VALGRIND" = ""],[
+ AS_IF([test "$enable_valgrind" = "yes"],[
+ AC_MSG_ERROR([Could not find valgrind; either install it or reconfigure with --disable-valgrind])
+ ],[
+ enable_valgrind=no
+ ])
+ ],[
+ enable_valgrind=yes
+ ])
+ ])
+
+ AM_CONDITIONAL([VALGRIND_ENABLED],[test "$enable_valgrind" = "yes"])
+ AC_SUBST([VALGRIND_ENABLED],[$enable_valgrind])
+
+ # Check for Valgrind tools we care about.
+ [valgrind_enabled_tools=]
+ m4_foreach([vgtool],[valgrind_tool_list],[
+ AC_ARG_ENABLE([valgrind-]vgtool,
+ m4_if(m4_defn([en_dflt_valgrind_]vgtool),[off],dnl
+[AS_HELP_STRING([--enable-valgrind-]vgtool, [Whether to use ]vgtool[ during the Valgrind tests])],dnl
+[AS_HELP_STRING([--disable-valgrind-]vgtool, [Whether to skip ]vgtool[ during the Valgrind tests])]),
+ [enable_valgrind_]vgtool[=$enableval],
+ [enable_valgrind_]vgtool[=])
+ AS_IF([test "$enable_valgrind" = "no"],[
+ enable_valgrind_]vgtool[=no],
+ [test "$enable_valgrind_]vgtool[" ]dnl
+m4_if(m4_defn([en_dflt_valgrind_]vgtool), [off], [= "yes"], [!= "no"]),[
+ AC_CACHE_CHECK([for Valgrind tool ]vgtool,
+ [ax_cv_valgrind_tool_]vgtool,[
+ ax_cv_valgrind_tool_]vgtool[=no
+ m4_set_contains([valgrind_exp_tool_set],vgtool,
+ [m4_define([vgtoolx],[exp-]vgtool)],
+ [m4_define([vgtoolx],vgtool)])
+ AS_IF([`$VALGRIND --tool=]vgtoolx[ --help >/dev/null 2>&1`],[
+ ax_cv_valgrind_tool_]vgtool[=yes
+ ])
+ ])
+ AS_IF([test "$ax_cv_valgrind_tool_]vgtool[" = "no"],[
+ AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[
+ AC_MSG_ERROR([Valgrind does not support ]vgtool[; reconfigure with --disable-valgrind-]vgtool)
+ ],[
+ enable_valgrind_]vgtool[=no
+ ])
+ ],[
+ enable_valgrind_]vgtool[=yes
+ ])
+ ])
+ AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[
+ valgrind_enabled_tools="$valgrind_enabled_tools ]m4_bpatsubst(vgtool,[^exp-])["
+ ])
+ AC_SUBST([ENABLE_VALGRIND_]vgtool,[$enable_valgrind_]vgtool)
+ ])
+ AC_SUBST([valgrind_tools],["]m4_join([ ], valgrind_tool_list)["])
+ AC_SUBST([valgrind_enabled_tools],[$valgrind_enabled_tools])
+
+[VALGRIND_CHECK_RULES='
+# Valgrind check
+#
+# Optional:
+# - VALGRIND_SUPPRESSIONS_FILES: Space-separated list of Valgrind suppressions
+# files to load. (Default: empty)
+# - VALGRIND_FLAGS: General flags to pass to all Valgrind tools.
+# (Default: --num-callers=30)
+# - VALGRIND_$toolname_FLAGS: Flags to pass to Valgrind $toolname (one of:
+# memcheck, helgrind, drd, sgcheck). (Default: various)
+
+# Optional variables
+VALGRIND_SUPPRESSIONS ?= $(addprefix --suppressions=,$(VALGRIND_SUPPRESSIONS_FILES))
+VALGRIND_FLAGS ?= --num-callers=30
+VALGRIND_memcheck_FLAGS ?= --leak-check=full --show-reachable=no
+VALGRIND_helgrind_FLAGS ?= --history-level=approx
+VALGRIND_drd_FLAGS ?=
+VALGRIND_sgcheck_FLAGS ?=
+
+# Internal use
+valgrind_log_files = $(addprefix test-suite-,$(addsuffix .log,$(valgrind_tools)))
+
+valgrind_memcheck_flags = --tool=memcheck $(VALGRIND_memcheck_FLAGS)
+valgrind_helgrind_flags = --tool=helgrind $(VALGRIND_helgrind_FLAGS)
+valgrind_drd_flags = --tool=drd $(VALGRIND_drd_FLAGS)
+valgrind_sgcheck_flags = --tool=exp-sgcheck $(VALGRIND_sgcheck_FLAGS)
+
+valgrind_quiet = $(valgrind_quiet_$(V))
+valgrind_quiet_ = $(valgrind_quiet_$(AM_DEFAULT_VERBOSITY))
+valgrind_quiet_0 = --quiet
+valgrind_v_use = $(valgrind_v_use_$(V))
+valgrind_v_use_ = $(valgrind_v_use_$(AM_DEFAULT_VERBOSITY))
+valgrind_v_use_0 = @echo " USE " $(patsubst check-valgrind-%-am,%,$''@):;
+
+# Support running with and without libtool.
+ifneq ($(LIBTOOL),)
+valgrind_lt = $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=execute
+else
+valgrind_lt =
+endif
+
+# Use recursive makes in order to ignore errors during check
+check-valgrind-am:
+ifeq ($(VALGRIND_ENABLED),yes)
+ $(A''M_V_at)$(MAKE) $(AM_MAKEFLAGS) -k \
+ $(foreach tool, $(valgrind_enabled_tools), check-valgrind-$(tool))
+else
+ @echo "Need to reconfigure with --enable-valgrind"
+endif
+
+# Valgrind running
+VALGRIND_TESTS_ENVIRONMENT = \
+ $(TESTS_ENVIRONMENT) \
+ env VALGRIND=$(VALGRIND) \
+ G_SLICE=always-malloc,debug-blocks \
+ G_DEBUG=fatal-warnings,fatal-criticals,gc-friendly
+
+VALGRIND_LOG_COMPILER = \
+ $(valgrind_lt) \
+ $(VALGRIND) $(VALGRIND_SUPPRESSIONS) --error-exitcode=1 $(VALGRIND_FLAGS)
+
+define valgrind_tool_rule
+check-valgrind-$(1)-am:
+ifeq ($$(VALGRIND_ENABLED)-$$(ENABLE_VALGRIND_$(1)),yes-yes)
+ifneq ($$(TESTS),)
+ $$(valgrind_v_use)$$(MAKE) check-TESTS \
+ TESTS_ENVIRONMENT="$$(VALGRIND_TESTS_ENVIRONMENT)" \
+ LOG_COMPILER="$$(VALGRIND_LOG_COMPILER)" \
+ LOG_FLAGS="$$(valgrind_$(1)_flags)" \
+ TEST_SUITE_LOG=test-suite-$(1).log
+endif
+else ifeq ($$(VALGRIND_ENABLED),yes)
+ @echo "Need to reconfigure with --enable-valgrind-$(1)"
+else
+ @echo "Need to reconfigure with --enable-valgrind"
+endif
+endef
+
+$(foreach tool,$(valgrind_tools),$(eval $(call valgrind_tool_rule,$(tool))))
+
+A''M_DISTCHECK_CONFIGURE_FLAGS ?=
+A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-valgrind
+
+MOSTLYCLEANFILES ?=
+MOSTLYCLEANFILES += $(valgrind_log_files)
+
+.PHONY: check-valgrind $(add-prefix check-valgrind-,$(valgrind_tools))
+']
+
+ AC_SUBST([VALGRIND_CHECK_RULES])
+ m4_ifdef([_AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE([VALGRIND_CHECK_RULES])])
+])
--- /dev/null
+## Autoconf macros for working with Guile.
+##
+## Copyright (C) 1998,2001, 2006, 2010, 2012, 2013, 2014 Free Software Foundation, Inc.
+##
+## This library is free software; you can redistribute it and/or
+## modify it under the terms of the GNU Lesser General Public License
+## as published by the Free Software Foundation; either version 3 of
+## the License, or (at your option) any later version.
+##
+## This library is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+## Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public
+## License along with this library; if not, write to the Free Software
+## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+## 02110-1301 USA
+
+# serial 10
+
+## Index
+## -----
+##
+## GUILE_PKG -- find Guile development files
+## GUILE_PROGS -- set paths to Guile interpreter, config and tool programs
+## GUILE_FLAGS -- set flags for compiling and linking with Guile
+## GUILE_SITE_DIR -- find path to Guile "site" directories
+## GUILE_CHECK -- evaluate Guile Scheme code and capture the return value
+## GUILE_MODULE_CHECK -- check feature of a Guile Scheme module
+## GUILE_MODULE_AVAILABLE -- check availability of a Guile Scheme module
+## GUILE_MODULE_REQUIRED -- fail if a Guile Scheme module is unavailable
+## GUILE_MODULE_EXPORTS -- check if a module exports a variable
+## GUILE_MODULE_REQUIRED_EXPORT -- fail if a module doesn't export a variable
+
+## Code
+## ----
+
+## NOTE: Comments preceding an AC_DEFUN (starting from "Usage:") are massaged
+## into doc/ref/autoconf-macros.texi (see Makefile.am in that directory).
+
+# GUILE_PKG -- find Guile development files
+#
+# Usage: GUILE_PKG([VERSIONS])
+#
+# This macro runs the @code{pkg-config} tool to find development files
+# for an available version of Guile.
+#
+# By default, this macro will search for the latest stable version of
+# Guile (e.g. 3.0), falling back to the previous stable version
+# (e.g. 2.2) if it is available. If no guile-@var{VERSION}.pc file is
+# found, an error is signalled. The found version is stored in
+# @var{GUILE_EFFECTIVE_VERSION}.
+#
+# If @code{GUILE_PROGS} was already invoked, this macro ensures that the
+# development files have the same effective version as the Guile
+# program.
+#
+# @var{GUILE_EFFECTIVE_VERSION} is marked for substitution, as by
+# @code{AC_SUBST}.
+#
+AC_DEFUN([GUILE_PKG],
+ [AC_REQUIRE([PKG_PROG_PKG_CONFIG])
+ if test "x$PKG_CONFIG" = x; then
+ AC_MSG_ERROR([pkg-config is missing, please install it])
+ fi
+ _guile_versions_to_search="m4_default([$1], [3.0 2.2 2.0])"
+ if test -n "$GUILE_EFFECTIVE_VERSION"; then
+ _guile_tmp=""
+ for v in $_guile_versions_to_search; do
+ if test "$v" = "$GUILE_EFFECTIVE_VERSION"; then
+ _guile_tmp=$v
+ fi
+ done
+ if test -z "$_guile_tmp"; then
+ AC_MSG_FAILURE([searching for guile development files for versions $_guile_versions_to_search, but previously found $GUILE version $GUILE_EFFECTIVE_VERSION])
+ fi
+ _guile_versions_to_search=$GUILE_EFFECTIVE_VERSION
+ fi
+ GUILE_EFFECTIVE_VERSION=""
+ _guile_errors=""
+ for v in $_guile_versions_to_search; do
+ if test -z "$GUILE_EFFECTIVE_VERSION"; then
+ AC_MSG_NOTICE([checking for guile $v])
+ PKG_CHECK_EXISTS([guile-$v], [GUILE_EFFECTIVE_VERSION=$v], [])
+ fi
+ done
+
+ if test -z "$GUILE_EFFECTIVE_VERSION"; then
+ AC_MSG_ERROR([
+No Guile development packages were found.
+
+Please verify that you have Guile installed. If you installed Guile
+from a binary distribution, please verify that you have also installed
+the development packages. If you installed it yourself, you might need
+to adjust your PKG_CONFIG_PATH; see the pkg-config man page for more.
+])
+ fi
+ AC_MSG_NOTICE([found guile $GUILE_EFFECTIVE_VERSION])
+ AC_SUBST([GUILE_EFFECTIVE_VERSION])
+ ])
+
+# GUILE_FLAGS -- set flags for compiling and linking with Guile
+#
+# Usage: GUILE_FLAGS
+#
+# This macro runs the @code{pkg-config} tool to find out how to compile
+# and link programs against Guile. It sets four variables:
+# @var{GUILE_CFLAGS}, @var{GUILE_LDFLAGS}, @var{GUILE_LIBS}, and
+# @var{GUILE_LTLIBS}.
+#
+# @var{GUILE_CFLAGS}: flags to pass to a C or C++ compiler to build code that
+# uses Guile header files. This is almost always just one or more @code{-I}
+# flags.
+#
+# @var{GUILE_LDFLAGS}: flags to pass to the compiler to link a program
+# against Guile. This includes @code{-lguile-@var{VERSION}} for the
+# Guile library itself, and may also include one or more @code{-L} flag
+# to tell the compiler where to find the libraries. But it does not
+# include flags that influence the program's runtime search path for
+# libraries, and will therefore lead to a program that fails to start,
+# unless all necessary libraries are installed in a standard location
+# such as @file{/usr/lib}.
+#
+# @var{GUILE_LIBS} and @var{GUILE_LTLIBS}: flags to pass to the compiler or to
+# libtool, respectively, to link a program against Guile. It includes flags
+# that augment the program's runtime search path for libraries, so that shared
+# libraries will be found at the location where they were during linking, even
+# in non-standard locations. @var{GUILE_LIBS} is to be used when linking the
+# program directly with the compiler, whereas @var{GUILE_LTLIBS} is to be used
+# when linking the program is done through libtool.
+#
+# The variables are marked for substitution, as by @code{AC_SUBST}.
+#
+AC_DEFUN([GUILE_FLAGS],
+ [AC_REQUIRE([GUILE_PKG])
+ PKG_CHECK_MODULES(GUILE, [guile-$GUILE_EFFECTIVE_VERSION])
+
+ dnl GUILE_CFLAGS and GUILE_LIBS are already defined and AC_SUBST'd by
+ dnl PKG_CHECK_MODULES. But GUILE_LIBS to pkg-config is GUILE_LDFLAGS
+ dnl to us.
+
+ GUILE_LDFLAGS=$GUILE_LIBS
+
+ dnl Determine the platform dependent parameters needed to use rpath.
+ dnl AC_LIB_LINKFLAGS_FROM_LIBS is defined in gnulib/m4/lib-link.m4 and needs
+ dnl the file gnulib/build-aux/config.rpath.
+ AC_LIB_LINKFLAGS_FROM_LIBS([GUILE_LIBS], [$GUILE_LDFLAGS], [])
+ GUILE_LIBS="$GUILE_LDFLAGS $GUILE_LIBS"
+ AC_LIB_LINKFLAGS_FROM_LIBS([GUILE_LTLIBS], [$GUILE_LDFLAGS], [yes])
+ GUILE_LTLIBS="$GUILE_LDFLAGS $GUILE_LTLIBS"
+
+ AC_SUBST([GUILE_EFFECTIVE_VERSION])
+ AC_SUBST([GUILE_CFLAGS])
+ AC_SUBST([GUILE_LDFLAGS])
+ AC_SUBST([GUILE_LIBS])
+ AC_SUBST([GUILE_LTLIBS])
+ ])
+
+# GUILE_SITE_DIR -- find path to Guile site directories
+#
+# Usage: GUILE_SITE_DIR
+#
+# This looks for Guile's "site" directories. The variable @var{GUILE_SITE} will
+# be set to Guile's "site" directory for Scheme source files (usually something
+# like PREFIX/share/guile/site). @var{GUILE_SITE_CCACHE} will be set to the
+# directory for compiled Scheme files also known as @code{.go} files
+# (usually something like
+# PREFIX/lib/guile/@var{GUILE_EFFECTIVE_VERSION}/site-ccache).
+# @var{GUILE_EXTENSION} will be set to the directory for compiled C extensions
+# (usually something like
+# PREFIX/lib/guile/@var{GUILE_EFFECTIVE_VERSION}/extensions). The latter two
+# are set to blank if the particular version of Guile does not support
+# them. Note that this macro will run the macros @code{GUILE_PKG} and
+# @code{GUILE_PROGS} if they have not already been run.
+#
+# The variables are marked for substitution, as by @code{AC_SUBST}.
+#
+AC_DEFUN([GUILE_SITE_DIR],
+ [AC_REQUIRE([GUILE_PKG])
+ AC_REQUIRE([GUILE_PROGS])
+ AC_MSG_CHECKING(for Guile site directory)
+ GUILE_SITE=`$PKG_CONFIG --print-errors --variable=sitedir guile-$GUILE_EFFECTIVE_VERSION`
+ AC_MSG_RESULT($GUILE_SITE)
+ if test "$GUILE_SITE" = ""; then
+ AC_MSG_FAILURE(sitedir not found)
+ fi
+ AC_SUBST(GUILE_SITE)
+ AC_MSG_CHECKING([for Guile site-ccache directory using pkgconfig])
+ GUILE_SITE_CCACHE=`$PKG_CONFIG --variable=siteccachedir guile-$GUILE_EFFECTIVE_VERSION`
+ if test "$GUILE_SITE_CCACHE" = ""; then
+ AC_MSG_RESULT(no)
+ AC_MSG_CHECKING([for Guile site-ccache directory using interpreter])
+ GUILE_SITE_CCACHE=`$GUILE -c "(display (if (defined? '%site-ccache-dir) (%site-ccache-dir) \"\"))"`
+ if test $? != "0" -o "$GUILE_SITE_CCACHE" = ""; then
+ AC_MSG_RESULT(no)
+ GUILE_SITE_CCACHE=""
+ AC_MSG_WARN([siteccachedir not found])
+ fi
+ fi
+ AC_MSG_RESULT($GUILE_SITE_CCACHE)
+ AC_SUBST([GUILE_SITE_CCACHE])
+ AC_MSG_CHECKING(for Guile extensions directory)
+ GUILE_EXTENSION=`$PKG_CONFIG --print-errors --variable=extensiondir guile-$GUILE_EFFECTIVE_VERSION`
+ AC_MSG_RESULT($GUILE_EXTENSION)
+ if test "$GUILE_EXTENSION" = ""; then
+ GUILE_EXTENSION=""
+ AC_MSG_WARN(extensiondir not found)
+ fi
+ AC_SUBST(GUILE_EXTENSION)
+ ])
+
+# GUILE_PROGS -- set paths to Guile interpreter, config and tool programs
+#
+# Usage: GUILE_PROGS([VERSION])
+#
+# This macro looks for programs @code{guile} and @code{guild}, setting
+# variables @var{GUILE} and @var{GUILD} to their paths, respectively.
+# The macro will attempt to find @code{guile} with the suffix of
+# @code{-X.Y}, followed by looking for it with the suffix @code{X.Y}, and
+# then fall back to looking for @code{guile} with no suffix. If
+# @code{guile} is still not found, signal an error. The suffix, if any,
+# that was required to find @code{guile} will be used for @code{guild}
+# as well.
+#
+# By default, this macro will search for the latest stable version of
+# Guile (e.g. 3.0). x.y or x.y.z versions can be specified. If an older
+# version is found, the macro will signal an error.
+#
+# The effective version of the found @code{guile} is set to
+# @var{GUILE_EFFECTIVE_VERSION}. This macro ensures that the effective
+# version is compatible with the result of a previous invocation of
+# @code{GUILE_FLAGS}, if any.
+#
+# As a legacy interface, it also looks for @code{guile-config} and
+# @code{guile-tools}, setting @var{GUILE_CONFIG} and @var{GUILE_TOOLS}.
+#
+# The variables are marked for substitution, as by @code{AC_SUBST}.
+#
+AC_DEFUN([GUILE_PROGS],
+ [_guile_required_version="m4_default([$1], [$GUILE_EFFECTIVE_VERSION])"
+ if test -z "$_guile_required_version"; then
+ _guile_required_version=3.0
+ fi
+
+ _guile_candidates=guile
+ _tmp=
+ for v in `echo "$_guile_required_version" | tr . ' '`; do
+ if test -n "$_tmp"; then _tmp=$_tmp.; fi
+ _tmp=$_tmp$v
+ _guile_candidates="guile-$_tmp guile$_tmp $_guile_candidates"
+ done
+
+ AC_PATH_PROGS(GUILE,[$_guile_candidates])
+ if test -z "$GUILE"; then
+ AC_MSG_ERROR([guile required but not found])
+ fi
+
+ _guile_suffix=`echo "$GUILE" | sed -e 's,^.*/guile\(.*\)$,\1,'`
+ _guile_effective_version=`$GUILE -c "(display (effective-version))"`
+ if test -z "$GUILE_EFFECTIVE_VERSION"; then
+ GUILE_EFFECTIVE_VERSION=$_guile_effective_version
+ elif test "$GUILE_EFFECTIVE_VERSION" != "$_guile_effective_version"; then
+ AC_MSG_ERROR([found development files for Guile $GUILE_EFFECTIVE_VERSION, but $GUILE has effective version $_guile_effective_version])
+ fi
+
+ _guile_major_version=`$GUILE -c "(display (major-version))"`
+ _guile_minor_version=`$GUILE -c "(display (minor-version))"`
+ _guile_micro_version=`$GUILE -c "(display (micro-version))"`
+ _guile_prog_version="$_guile_major_version.$_guile_minor_version.$_guile_micro_version"
+
+ AC_MSG_CHECKING([for Guile version >= $_guile_required_version])
+ _major_version=`echo $_guile_required_version | cut -d . -f 1`
+ _minor_version=`echo $_guile_required_version | cut -d . -f 2`
+ _micro_version=`echo $_guile_required_version | cut -d . -f 3`
+ if test "$_guile_major_version" -gt "$_major_version"; then
+ true
+ elif test "$_guile_major_version" -eq "$_major_version"; then
+ if test "$_guile_minor_version" -gt "$_minor_version"; then
+ true
+ elif test "$_guile_minor_version" -eq "$_minor_version"; then
+ if test -n "$_micro_version"; then
+ if test "$_guile_micro_version" -lt "$_micro_version"; then
+ AC_MSG_ERROR([Guile $_guile_required_version required, but $_guile_prog_version found])
+ fi
+ fi
+ elif test "$GUILE_EFFECTIVE_VERSION" = "$_major_version.$_minor_version" -a -z "$_micro_version"; then
+ # Allow prereleases that have the right effective version.
+ true
+ else
+ as_fn_error $? "Guile $_guile_required_version required, but $_guile_prog_version found" "$LINENO" 5
+ fi
+ elif test "$GUILE_EFFECTIVE_VERSION" = "$_major_version.$_minor_version" -a -z "$_micro_version"; then
+ # Allow prereleases that have the right effective version.
+ true
+ else
+ AC_MSG_ERROR([Guile $_guile_required_version required, but $_guile_prog_version found])
+ fi
+ AC_MSG_RESULT([$_guile_prog_version])
+
+ AC_PATH_PROG(GUILD,[guild$_guile_suffix])
+ AC_SUBST(GUILD)
+
+ AC_PATH_PROG(GUILE_CONFIG,[guile-config$_guile_suffix])
+ AC_SUBST(GUILE_CONFIG)
+ if test -n "$GUILD"; then
+ GUILE_TOOLS=$GUILD
+ else
+ AC_PATH_PROG(GUILE_TOOLS,[guile-tools$_guile_suffix])
+ fi
+ AC_SUBST(GUILE_TOOLS)
+ ])
+
+# GUILE_CHECK -- evaluate Guile Scheme code and capture the return value
+#
+# Usage: GUILE_CHECK_RETVAL(var,check)
+#
+# @var{var} is a shell variable name to be set to the return value.
+# @var{check} is a Guile Scheme expression, evaluated with "$GUILE -c", and
+# returning either 0 or non-#f to indicate the check passed.
+# Non-0 number or #f indicates failure.
+# Avoid using the character "#" since that confuses autoconf.
+#
+AC_DEFUN([GUILE_CHECK],
+ [AC_REQUIRE([GUILE_PROGS])
+ $GUILE -c "$2" > /dev/null 2>&1
+ $1=$?
+ ])
+
+# GUILE_MODULE_CHECK -- check feature of a Guile Scheme module
+#
+# Usage: GUILE_MODULE_CHECK(var,module,featuretest,description)
+#
+# @var{var} is a shell variable name to be set to "yes" or "no".
+# @var{module} is a list of symbols, like: (ice-9 common-list).
+# @var{featuretest} is an expression acceptable to GUILE_CHECK, q.v.
+# @var{description} is a present-tense verb phrase (passed to AC_MSG_CHECKING).
+#
+AC_DEFUN([GUILE_MODULE_CHECK],
+ [AC_MSG_CHECKING([if $2 $4])
+ GUILE_CHECK($1,(use-modules $2) (exit ((lambda () $3))))
+ if test "$$1" = "0" ; then $1=yes ; else $1=no ; fi
+ AC_MSG_RESULT($$1)
+ ])
+
+# GUILE_MODULE_AVAILABLE -- check availability of a Guile Scheme module
+#
+# Usage: GUILE_MODULE_AVAILABLE(var,module)
+#
+# @var{var} is a shell variable name to be set to "yes" or "no".
+# @var{module} is a list of symbols, like: (ice-9 common-list).
+#
+AC_DEFUN([GUILE_MODULE_AVAILABLE],
+ [GUILE_MODULE_CHECK($1,$2,0,is available)
+ ])
+
+# GUILE_MODULE_REQUIRED -- fail if a Guile Scheme module is unavailable
+#
+# Usage: GUILE_MODULE_REQUIRED(symlist)
+#
+# @var{symlist} is a list of symbols, WITHOUT surrounding parens,
+# like: ice-9 common-list.
+#
+AC_DEFUN([GUILE_MODULE_REQUIRED],
+ [GUILE_MODULE_AVAILABLE(ac_guile_module_required, ($1))
+ if test "$ac_guile_module_required" = "no" ; then
+ AC_MSG_ERROR([required guile module not found: ($1)])
+ fi
+ ])
+
+# GUILE_MODULE_EXPORTS -- check if a module exports a variable
+#
+# Usage: GUILE_MODULE_EXPORTS(var,module,modvar)
+#
+# @var{var} is a shell variable to be set to "yes" or "no".
+# @var{module} is a list of symbols, like: (ice-9 common-list).
+# @var{modvar} is the Guile Scheme variable to check.
+#
+AC_DEFUN([GUILE_MODULE_EXPORTS],
+ [GUILE_MODULE_CHECK($1,$2,$3,exports `$3')
+ ])
+
+# GUILE_MODULE_REQUIRED_EXPORT -- fail if a module doesn't export a variable
+#
+# Usage: GUILE_MODULE_REQUIRED_EXPORT(module,modvar)
+#
+# @var{module} is a list of symbols, like: (ice-9 common-list).
+# @var{modvar} is the Guile Scheme variable to check.
+#
+AC_DEFUN([GUILE_MODULE_REQUIRED_EXPORT],
+ [GUILE_MODULE_EXPORTS(guile_module_required_export,$1,$2)
+ if test "$guile_module_required_export" = "no" ; then
+ AC_MSG_ERROR([module $1 does not export $2; required])
+ fi
+ ])
+
+## guile.m4 ends here
--- /dev/null
+# host-cpu-c-abi.m4 serial 13
+dnl Copyright (C) 2002-2020 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+dnl From Bruno Haible and Sam Steingold.
+
+dnl Sets the HOST_CPU variable to the canonical name of the CPU.
+dnl Sets the HOST_CPU_C_ABI variable to the canonical name of the CPU with its
+dnl C language ABI (application binary interface).
+dnl Also defines __${HOST_CPU}__ and __${HOST_CPU_C_ABI}__ as C macros in
+dnl config.h.
+dnl
+dnl This canonical name can be used to select a particular assembly language
+dnl source file that will interoperate with C code on the given host.
+dnl
+dnl For example:
+dnl * 'i386' and 'sparc' are different canonical names, because code for i386
+dnl will not run on SPARC CPUs and vice versa. They have different
+dnl instruction sets.
+dnl * 'sparc' and 'sparc64' are different canonical names, because code for
+dnl 'sparc' and code for 'sparc64' cannot be linked together: 'sparc' code
+dnl contains 32-bit instructions, whereas 'sparc64' code contains 64-bit
+dnl instructions. A process on a SPARC CPU can be in 32-bit mode or in 64-bit
+dnl mode, but not both.
+dnl * 'mips' and 'mipsn32' are different canonical names, because they use
+dnl different argument passing and return conventions for C functions, and
+dnl although the instruction set of 'mips' is a large subset of the
+dnl instruction set of 'mipsn32'.
+dnl * 'mipsn32' and 'mips64' are different canonical names, because they use
+dnl different sizes for the C types like 'int' and 'void *', and although
+dnl the instruction sets of 'mipsn32' and 'mips64' are the same.
+dnl * The same canonical name is used for different endiannesses. You can
+dnl determine the endianness through preprocessor symbols:
+dnl - 'arm': test __ARMEL__.
+dnl - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL.
+dnl - 'powerpc64': test _BIG_ENDIAN vs. _LITTLE_ENDIAN.
+dnl * The same name 'i386' is used for CPUs of type i386, i486, i586
+dnl (Pentium), AMD K7, Pentium II, Pentium IV, etc., because
+dnl - Instructions that do not exist on all of these CPUs (cmpxchg,
+dnl MMX, SSE, SSE2, 3DNow! etc.) are not frequently used. If your
+dnl assembly language source files use such instructions, you will
+dnl need to make the distinction.
+dnl - Speed of execution of the common instruction set is reasonable across
+dnl the entire family of CPUs. If you have assembly language source files
+dnl that are optimized for particular CPU types (like GNU gmp has), you
+dnl will need to make the distinction.
+dnl See <https://en.wikipedia.org/wiki/X86_instruction_listings>.
+AC_DEFUN([gl_HOST_CPU_C_ABI],
+[
+ AC_REQUIRE([AC_CANONICAL_HOST])
+ AC_REQUIRE([gl_C_ASM])
+ AC_CACHE_CHECK([host CPU and C ABI], [gl_cv_host_cpu_c_abi],
+ [case "$host_cpu" in
+
+changequote(,)dnl
+ i[34567]86 )
+changequote([,])dnl
+ gl_cv_host_cpu_c_abi=i386
+ ;;
+
+ x86_64 )
+ # On x86_64 systems, the C compiler may be generating code in one of
+ # these ABIs:
+ # - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64.
+ # - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64
+ # with native Windows (mingw, MSVC).
+ # - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32.
+ # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if (defined __x86_64__ || defined __amd64__ \
+ || defined _M_X64 || defined _M_AMD64)
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __ILP32__ || defined _ILP32
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=x86_64-x32],
+ [gl_cv_host_cpu_c_abi=x86_64])],
+ [gl_cv_host_cpu_c_abi=i386])
+ ;;
+
+changequote(,)dnl
+ alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] )
+changequote([,])dnl
+ gl_cv_host_cpu_c_abi=alpha
+ ;;
+
+ arm* | aarch64 )
+ # Assume arm with EABI.
+ # On arm64 systems, the C compiler may be generating code in one of
+ # these ABIs:
+ # - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64.
+ # - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32.
+ # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#ifdef __aarch64__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __ILP32__ || defined _ILP32
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=arm64-ilp32],
+ [gl_cv_host_cpu_c_abi=arm64])],
+ [# Don't distinguish little-endian and big-endian arm, since they
+ # don't require different machine code for simple operations and
+ # since the user can distinguish them through the preprocessor
+ # defines __ARMEL__ vs. __ARMEB__.
+ # But distinguish arm which passes floating-point arguments and
+ # return values in integer registers (r0, r1, ...) - this is
+ # gcc -mfloat-abi=soft or gcc -mfloat-abi=softfp - from arm which
+ # passes them in float registers (s0, s1, ...) and double registers
+ # (d0, d1, ...) - this is gcc -mfloat-abi=hard. GCC 4.6 or newer
+ # sets the preprocessor defines __ARM_PCS (for the first case) and
+ # __ARM_PCS_VFP (for the second case), but older GCC does not.
+ echo 'double ddd; void func (double dd) { ddd = dd; }' > conftest.c
+ # Look for a reference to the register d0 in the .s file.
+ AC_TRY_COMMAND(${CC-cc} $CFLAGS $CPPFLAGS $gl_c_asm_opt conftest.c) >/dev/null 2>&1
+ if LC_ALL=C grep 'd0,' conftest.$gl_asmext >/dev/null; then
+ gl_cv_host_cpu_c_abi=armhf
+ else
+ gl_cv_host_cpu_c_abi=arm
+ fi
+ rm -f conftest*
+ ])
+ ;;
+
+ hppa1.0 | hppa1.1 | hppa2.0* | hppa64 )
+ # On hppa, the C compiler may be generating 32-bit code or 64-bit
+ # code. In the latter case, it defines _LP64 and __LP64__.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#ifdef __LP64__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=hppa64],
+ [gl_cv_host_cpu_c_abi=hppa])
+ ;;
+
+ ia64* )
+ # On ia64 on HP-UX, the C compiler may be generating 64-bit code or
+ # 32-bit code. In the latter case, it defines _ILP32.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#ifdef _ILP32
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=ia64-ilp32],
+ [gl_cv_host_cpu_c_abi=ia64])
+ ;;
+
+ mips* )
+ # We should also check for (_MIPS_SZPTR == 64), but gcc keeps this
+ # at 32.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64)
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=mips64],
+ [# In the n32 ABI, _ABIN32 is defined, _ABIO32 is not defined (but
+ # may later get defined by <sgidefs.h>), and _MIPS_SIM == _ABIN32.
+ # In the 32 ABI, _ABIO32 is defined, _ABIN32 is not defined (but
+ # may later get defined by <sgidefs.h>), and _MIPS_SIM == _ABIO32.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if (_MIPS_SIM == _ABIN32)
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=mipsn32],
+ [gl_cv_host_cpu_c_abi=mips])])
+ ;;
+
+ powerpc* )
+ # Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD.
+ # No need to distinguish them here; the caller may distinguish
+ # them based on the OS.
+ # On powerpc64 systems, the C compiler may still be generating
+ # 32-bit code. And on powerpc-ibm-aix systems, the C compiler may
+ # be generating 64-bit code.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __powerpc64__ || defined _ARCH_PPC64
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [# On powerpc64, there are two ABIs on Linux: The AIX compatible
+ # one and the ELFv2 one. The latter defines _CALL_ELF=2.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined _CALL_ELF && _CALL_ELF == 2
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=powerpc64-elfv2],
+ [gl_cv_host_cpu_c_abi=powerpc64])
+ ],
+ [gl_cv_host_cpu_c_abi=powerpc])
+ ;;
+
+ rs6000 )
+ gl_cv_host_cpu_c_abi=powerpc
+ ;;
+
+ riscv32 | riscv64 )
+ # There are 2 architectures (with variants): rv32* and rv64*.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if __riscv_xlen == 64
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [cpu=riscv64],
+ [cpu=riscv32])
+ # There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d.
+ # Size of 'long' and 'void *':
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __LP64__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [main_abi=lp64],
+ [main_abi=ilp32])
+ # Float ABIs:
+ # __riscv_float_abi_double:
+ # 'float' and 'double' are passed in floating-point registers.
+ # __riscv_float_abi_single:
+ # 'float' are passed in floating-point registers.
+ # __riscv_float_abi_soft:
+ # No values are passed in floating-point registers.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __riscv_float_abi_double
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [float_abi=d],
+ [AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __riscv_float_abi_single
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [float_abi=f],
+ [float_abi=''])
+ ])
+ gl_cv_host_cpu_c_abi="${cpu}-${main_abi}${float_abi}"
+ ;;
+
+ s390* )
+ # On s390x, the C compiler may be generating 64-bit (= s390x) code
+ # or 31-bit (= s390) code.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __LP64__ || defined __s390x__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=s390x],
+ [gl_cv_host_cpu_c_abi=s390])
+ ;;
+
+ sparc | sparc64 )
+ # UltraSPARCs running Linux have `uname -m` = "sparc64", but the
+ # C compiler still generates 32-bit code.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __sparcv9 || defined __arch64__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi=sparc64],
+ [gl_cv_host_cpu_c_abi=sparc])
+ ;;
+
+ *)
+ gl_cv_host_cpu_c_abi="$host_cpu"
+ ;;
+ esac
+ ])
+
+ dnl In most cases, $HOST_CPU and $HOST_CPU_C_ABI are the same.
+ HOST_CPU=`echo "$gl_cv_host_cpu_c_abi" | sed -e 's/-.*//'`
+ HOST_CPU_C_ABI="$gl_cv_host_cpu_c_abi"
+ AC_SUBST([HOST_CPU])
+ AC_SUBST([HOST_CPU_C_ABI])
+
+ # This was
+ # AC_DEFINE_UNQUOTED([__${HOST_CPU}__])
+ # AC_DEFINE_UNQUOTED([__${HOST_CPU_C_ABI}__])
+ # earlier, but KAI C++ 3.2d doesn't like this.
+ sed -e 's/-/_/g' >> confdefs.h <<EOF
+#ifndef __${HOST_CPU}__
+#define __${HOST_CPU}__ 1
+#endif
+#ifndef __${HOST_CPU_C_ABI}__
+#define __${HOST_CPU_C_ABI}__ 1
+#endif
+EOF
+ AH_TOP([/* CPU and C ABI indicator */
+#ifndef __i386__
+#undef __i386__
+#endif
+#ifndef __x86_64_x32__
+#undef __x86_64_x32__
+#endif
+#ifndef __x86_64__
+#undef __x86_64__
+#endif
+#ifndef __alpha__
+#undef __alpha__
+#endif
+#ifndef __arm__
+#undef __arm__
+#endif
+#ifndef __armhf__
+#undef __armhf__
+#endif
+#ifndef __arm64_ilp32__
+#undef __arm64_ilp32__
+#endif
+#ifndef __arm64__
+#undef __arm64__
+#endif
+#ifndef __hppa__
+#undef __hppa__
+#endif
+#ifndef __hppa64__
+#undef __hppa64__
+#endif
+#ifndef __ia64_ilp32__
+#undef __ia64_ilp32__
+#endif
+#ifndef __ia64__
+#undef __ia64__
+#endif
+#ifndef __m68k__
+#undef __m68k__
+#endif
+#ifndef __mips__
+#undef __mips__
+#endif
+#ifndef __mipsn32__
+#undef __mipsn32__
+#endif
+#ifndef __mips64__
+#undef __mips64__
+#endif
+#ifndef __powerpc__
+#undef __powerpc__
+#endif
+#ifndef __powerpc64__
+#undef __powerpc64__
+#endif
+#ifndef __powerpc64_elfv2__
+#undef __powerpc64_elfv2__
+#endif
+#ifndef __riscv32__
+#undef __riscv32__
+#endif
+#ifndef __riscv64__
+#undef __riscv64__
+#endif
+#ifndef __riscv32_ilp32__
+#undef __riscv32_ilp32__
+#endif
+#ifndef __riscv32_ilp32f__
+#undef __riscv32_ilp32f__
+#endif
+#ifndef __riscv32_ilp32d__
+#undef __riscv32_ilp32d__
+#endif
+#ifndef __riscv64_ilp32__
+#undef __riscv64_ilp32__
+#endif
+#ifndef __riscv64_ilp32f__
+#undef __riscv64_ilp32f__
+#endif
+#ifndef __riscv64_ilp32d__
+#undef __riscv64_ilp32d__
+#endif
+#ifndef __riscv64_lp64__
+#undef __riscv64_lp64__
+#endif
+#ifndef __riscv64_lp64f__
+#undef __riscv64_lp64f__
+#endif
+#ifndef __riscv64_lp64d__
+#undef __riscv64_lp64d__
+#endif
+#ifndef __s390__
+#undef __s390__
+#endif
+#ifndef __s390x__
+#undef __s390x__
+#endif
+#ifndef __sh__
+#undef __sh__
+#endif
+#ifndef __sparc__
+#undef __sparc__
+#endif
+#ifndef __sparc64__
+#undef __sparc64__
+#endif
+])
+
+])
+
+
+dnl Sets the HOST_CPU_C_ABI_32BIT variable to 'yes' if the C language ABI
+dnl (application binary interface) is a 32-bit one, to 'no' if it is a 64-bit
+dnl one, or to 'unknown' if unknown.
+dnl This is a simplified variant of gl_HOST_CPU_C_ABI.
+AC_DEFUN([gl_HOST_CPU_C_ABI_32BIT],
+[
+ AC_REQUIRE([AC_CANONICAL_HOST])
+ AC_CACHE_CHECK([32-bit host C ABI], [gl_cv_host_cpu_c_abi_32bit],
+ [if test -n "$gl_cv_host_cpu_c_abi"; then
+ case "$gl_cv_host_cpu_c_abi" in
+ i386 | x86_64-x32 | arm | armhf | arm64-ilp32 | hppa | ia64-ilp32 | mips | mipsn32 | powerpc | riscv*-ilp32* | s390 | sparc)
+ gl_cv_host_cpu_c_abi_32bit=yes ;;
+ x86_64 | alpha | arm64 | hppa64 | ia64 | mips64 | powerpc64 | powerpc64-elfv2 | riscv*-lp64* | s390x | sparc64 )
+ gl_cv_host_cpu_c_abi_32bit=no ;;
+ *)
+ gl_cv_host_cpu_c_abi_32bit=unknown ;;
+ esac
+ else
+ case "$host_cpu" in
+
+ # CPUs that only support a 32-bit ABI.
+ arc \
+ | bfin \
+ | cris* \
+ | csky \
+ | epiphany \
+ | ft32 \
+ | h8300 \
+ | m68k \
+ | microblaze | microblazeel \
+ | nds32 | nds32le | nds32be \
+ | nios2 | nios2eb | nios2el \
+ | or1k* \
+ | or32 \
+ | sh | sh[1234] | sh[1234]e[lb] \
+ | tic6x \
+ | xtensa* )
+ gl_cv_host_cpu_c_abi_32bit=yes
+ ;;
+
+ # CPUs that only support a 64-bit ABI.
+changequote(,)dnl
+ alpha | alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] \
+ | mmix )
+changequote([,])dnl
+ gl_cv_host_cpu_c_abi_32bit=no
+ ;;
+
+changequote(,)dnl
+ i[34567]86 )
+changequote([,])dnl
+ gl_cv_host_cpu_c_abi_32bit=yes
+ ;;
+
+ x86_64 )
+ # On x86_64 systems, the C compiler may be generating code in one of
+ # these ABIs:
+ # - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64.
+ # - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64
+ # with native Windows (mingw, MSVC).
+ # - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32.
+ # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if (defined __x86_64__ || defined __amd64__ \
+ || defined _M_X64 || defined _M_AMD64) \
+ && !(defined __ILP32__ || defined _ILP32)
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=no],
+ [gl_cv_host_cpu_c_abi_32bit=yes])
+ ;;
+
+ arm* | aarch64 )
+ # Assume arm with EABI.
+ # On arm64 systems, the C compiler may be generating code in one of
+ # these ABIs:
+ # - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64.
+ # - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32.
+ # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __aarch64__ && !(defined __ILP32__ || defined _ILP32)
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=no],
+ [gl_cv_host_cpu_c_abi_32bit=yes])
+ ;;
+
+ hppa1.0 | hppa1.1 | hppa2.0* | hppa64 )
+ # On hppa, the C compiler may be generating 32-bit code or 64-bit
+ # code. In the latter case, it defines _LP64 and __LP64__.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#ifdef __LP64__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=no],
+ [gl_cv_host_cpu_c_abi_32bit=yes])
+ ;;
+
+ ia64* )
+ # On ia64 on HP-UX, the C compiler may be generating 64-bit code or
+ # 32-bit code. In the latter case, it defines _ILP32.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#ifdef _ILP32
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=yes],
+ [gl_cv_host_cpu_c_abi_32bit=no])
+ ;;
+
+ mips* )
+ # We should also check for (_MIPS_SZPTR == 64), but gcc keeps this
+ # at 32.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64)
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=no],
+ [gl_cv_host_cpu_c_abi_32bit=yes])
+ ;;
+
+ powerpc* )
+ # Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD.
+ # No need to distinguish them here; the caller may distinguish
+ # them based on the OS.
+ # On powerpc64 systems, the C compiler may still be generating
+ # 32-bit code. And on powerpc-ibm-aix systems, the C compiler may
+ # be generating 64-bit code.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __powerpc64__ || defined _ARCH_PPC64
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=no],
+ [gl_cv_host_cpu_c_abi_32bit=yes])
+ ;;
+
+ rs6000 )
+ gl_cv_host_cpu_c_abi_32bit=yes
+ ;;
+
+ riscv32 | riscv64 )
+ # There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d.
+ # Size of 'long' and 'void *':
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __LP64__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=no],
+ [gl_cv_host_cpu_c_abi_32bit=yes])
+ ;;
+
+ s390* )
+ # On s390x, the C compiler may be generating 64-bit (= s390x) code
+ # or 31-bit (= s390) code.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __LP64__ || defined __s390x__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=no],
+ [gl_cv_host_cpu_c_abi_32bit=yes])
+ ;;
+
+ sparc | sparc64 )
+ # UltraSPARCs running Linux have `uname -m` = "sparc64", but the
+ # C compiler still generates 32-bit code.
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __sparcv9 || defined __arch64__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_host_cpu_c_abi_32bit=no],
+ [gl_cv_host_cpu_c_abi_32bit=yes])
+ ;;
+
+ *)
+ gl_cv_host_cpu_c_abi_32bit=unknown
+ ;;
+ esac
+ fi
+ ])
+
+ HOST_CPU_C_ABI_32BIT="$gl_cv_host_cpu_c_abi_32bit"
+])
--- /dev/null
+# lib-ld.m4 serial 9
+dnl Copyright (C) 1996-2003, 2009-2019 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+dnl Subroutines of libtool.m4,
+dnl with replacements s/_*LT_PATH/AC_LIB_PROG/ and s/lt_/acl_/ to avoid
+dnl collision with libtool.m4.
+
+dnl From libtool-2.4. Sets the variable with_gnu_ld to yes or no.
+AC_DEFUN([AC_LIB_PROG_LD_GNU],
+[AC_CACHE_CHECK([if the linker ($LD) is GNU ld], [acl_cv_prog_gnu_ld],
+[# I'd rather use --version here, but apparently some GNU lds only accept -v.
+case `$LD -v 2>&1 </dev/null` in
+*GNU* | *'with BFD'*)
+ acl_cv_prog_gnu_ld=yes
+ ;;
+*)
+ acl_cv_prog_gnu_ld=no
+ ;;
+esac])
+with_gnu_ld=$acl_cv_prog_gnu_ld
+])
+
+dnl From libtool-2.4. Sets the variable LD.
+AC_DEFUN([AC_LIB_PROG_LD],
+[AC_REQUIRE([AC_PROG_CC])dnl
+AC_REQUIRE([AC_CANONICAL_HOST])dnl
+
+AC_ARG_WITH([gnu-ld],
+ [AS_HELP_STRING([--with-gnu-ld],
+ [assume the C compiler uses GNU ld [default=no]])],
+ [test "$withval" = no || with_gnu_ld=yes],
+ [with_gnu_ld=no])dnl
+
+# Prepare PATH_SEPARATOR.
+# The user is always right.
+if test "${PATH_SEPARATOR+set}" != set; then
+ # Determine PATH_SEPARATOR by trying to find /bin/sh in a PATH which
+ # contains only /bin. Note that ksh looks also at the FPATH variable,
+ # so we have to set that as well for the test.
+ PATH_SEPARATOR=:
+ (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 \
+ && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 \
+ || PATH_SEPARATOR=';'
+ }
+fi
+
+if test -n "$LD"; then
+ AC_MSG_CHECKING([for ld])
+elif test "$GCC" = yes; then
+ AC_MSG_CHECKING([for ld used by $CC])
+elif test "$with_gnu_ld" = yes; then
+ AC_MSG_CHECKING([for GNU ld])
+else
+ AC_MSG_CHECKING([for non-GNU ld])
+fi
+if test -n "$LD"; then
+ # Let the user override the test with a path.
+ :
+else
+ AC_CACHE_VAL([acl_cv_path_LD],
+ [
+ acl_cv_path_LD= # Final result of this test
+ ac_prog=ld # Program to search in $PATH
+ if test "$GCC" = yes; then
+ # Check if gcc -print-prog-name=ld gives a path.
+ case $host in
+ *-*-mingw*)
+ # gcc leaves a trailing carriage return which upsets mingw
+ acl_output=`($CC -print-prog-name=ld) 2>&5 | tr -d '\015'` ;;
+ *)
+ acl_output=`($CC -print-prog-name=ld) 2>&5` ;;
+ esac
+ case $acl_output in
+ # Accept absolute paths.
+ [[\\/]]* | ?:[[\\/]]*)
+ re_direlt='/[[^/]][[^/]]*/\.\./'
+ # Canonicalize the pathname of ld
+ acl_output=`echo "$acl_output" | sed 's%\\\\%/%g'`
+ while echo "$acl_output" | grep "$re_direlt" > /dev/null 2>&1; do
+ acl_output=`echo $acl_output | sed "s%$re_direlt%/%"`
+ done
+ # Got the pathname. No search in PATH is needed.
+ acl_cv_path_LD="$acl_output"
+ ac_prog=
+ ;;
+ "")
+ # If it fails, then pretend we aren't using GCC.
+ ;;
+ *)
+ # If it is relative, then search for the first ld in PATH.
+ with_gnu_ld=unknown
+ ;;
+ esac
+ fi
+ if test -n "$ac_prog"; then
+ # Search for $ac_prog in $PATH.
+ acl_save_ifs="$IFS"; IFS=$PATH_SEPARATOR
+ for ac_dir in $PATH; do
+ IFS="$acl_save_ifs"
+ test -z "$ac_dir" && ac_dir=.
+ if test -f "$ac_dir/$ac_prog" || test -f "$ac_dir/$ac_prog$ac_exeext"; then
+ acl_cv_path_LD="$ac_dir/$ac_prog"
+ # Check to see if the program is GNU ld. I'd rather use --version,
+ # but apparently some variants of GNU ld only accept -v.
+ # Break only if it was the GNU/non-GNU ld that we prefer.
+ case `"$acl_cv_path_LD" -v 2>&1 </dev/null` in
+ *GNU* | *'with BFD'*)
+ test "$with_gnu_ld" != no && break
+ ;;
+ *)
+ test "$with_gnu_ld" != yes && break
+ ;;
+ esac
+ fi
+ done
+ IFS="$acl_save_ifs"
+ fi
+ case $host in
+ *-*-aix*)
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __powerpc64__ || defined _ARCH_PPC64
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [# The compiler produces 64-bit code. Add option '-b64' so that the
+ # linker groks 64-bit object files.
+ case "$acl_cv_path_LD " in
+ *" -b64 "*) ;;
+ *) acl_cv_path_LD="$acl_cv_path_LD -b64" ;;
+ esac
+ ], [])
+ ;;
+ sparc64-*-netbsd*)
+ AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#if defined __sparcv9 || defined __arch64__
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [],
+ [# The compiler produces 32-bit code. Add option '-m elf32_sparc'
+ # so that the linker groks 32-bit object files.
+ case "$acl_cv_path_LD " in
+ *" -m elf32_sparc "*) ;;
+ *) acl_cv_path_LD="$acl_cv_path_LD -m elf32_sparc" ;;
+ esac
+ ])
+ ;;
+ esac
+ ])
+ LD="$acl_cv_path_LD"
+fi
+if test -n "$LD"; then
+ AC_MSG_RESULT([$LD])
+else
+ AC_MSG_RESULT([no])
+ AC_MSG_ERROR([no acceptable ld found in \$PATH])
+fi
+AC_LIB_PROG_LD_GNU
+])
--- /dev/null
+# lib-link.m4 serial 28
+dnl Copyright (C) 2001-2019 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+dnl From Bruno Haible.
+
+AC_PREREQ([2.61])
+
+dnl AC_LIB_LINKFLAGS(name [, dependencies]) searches for libname and
+dnl the libraries corresponding to explicit and implicit dependencies.
+dnl Sets and AC_SUBSTs the LIB${NAME} and LTLIB${NAME} variables and
+dnl augments the CPPFLAGS variable.
+dnl Sets and AC_SUBSTs the LIB${NAME}_PREFIX variable to nonempty if libname
+dnl was found in ${LIB${NAME}_PREFIX}/$acl_libdirstem.
+AC_DEFUN([AC_LIB_LINKFLAGS],
+[
+ AC_REQUIRE([AC_LIB_PREPARE_PREFIX])
+ AC_REQUIRE([AC_LIB_RPATH])
+ pushdef([Name],[m4_translit([$1],[./+-], [____])])
+ pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-],
+ [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+ AC_CACHE_CHECK([how to link with lib[]$1], [ac_cv_lib[]Name[]_libs], [
+ AC_LIB_LINKFLAGS_BODY([$1], [$2])
+ ac_cv_lib[]Name[]_libs="$LIB[]NAME"
+ ac_cv_lib[]Name[]_ltlibs="$LTLIB[]NAME"
+ ac_cv_lib[]Name[]_cppflags="$INC[]NAME"
+ ac_cv_lib[]Name[]_prefix="$LIB[]NAME[]_PREFIX"
+ ])
+ LIB[]NAME="$ac_cv_lib[]Name[]_libs"
+ LTLIB[]NAME="$ac_cv_lib[]Name[]_ltlibs"
+ INC[]NAME="$ac_cv_lib[]Name[]_cppflags"
+ LIB[]NAME[]_PREFIX="$ac_cv_lib[]Name[]_prefix"
+ AC_LIB_APPENDTOVAR([CPPFLAGS], [$INC]NAME)
+ AC_SUBST([LIB]NAME)
+ AC_SUBST([LTLIB]NAME)
+ AC_SUBST([LIB]NAME[_PREFIX])
+ dnl Also set HAVE_LIB[]NAME so that AC_LIB_HAVE_LINKFLAGS can reuse the
+ dnl results of this search when this library appears as a dependency.
+ HAVE_LIB[]NAME=yes
+ popdef([NAME])
+ popdef([Name])
+])
+
+dnl AC_LIB_HAVE_LINKFLAGS(name, dependencies, includes, testcode, [missing-message])
+dnl searches for libname and the libraries corresponding to explicit and
+dnl implicit dependencies, together with the specified include files and
+dnl the ability to compile and link the specified testcode. The missing-message
+dnl defaults to 'no' and may contain additional hints for the user.
+dnl If found, it sets and AC_SUBSTs HAVE_LIB${NAME}=yes and the LIB${NAME}
+dnl and LTLIB${NAME} variables and augments the CPPFLAGS variable, and
+dnl #defines HAVE_LIB${NAME} to 1. Otherwise, it sets and AC_SUBSTs
+dnl HAVE_LIB${NAME}=no and LIB${NAME} and LTLIB${NAME} to empty.
+dnl Sets and AC_SUBSTs the LIB${NAME}_PREFIX variable to nonempty if libname
+dnl was found in ${LIB${NAME}_PREFIX}/$acl_libdirstem.
+AC_DEFUN([AC_LIB_HAVE_LINKFLAGS],
+[
+ AC_REQUIRE([AC_LIB_PREPARE_PREFIX])
+ AC_REQUIRE([AC_LIB_RPATH])
+ pushdef([Name],[m4_translit([$1],[./+-], [____])])
+ pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-],
+ [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+
+ dnl Search for lib[]Name and define LIB[]NAME, LTLIB[]NAME and INC[]NAME
+ dnl accordingly.
+ AC_LIB_LINKFLAGS_BODY([$1], [$2])
+
+ dnl Add $INC[]NAME to CPPFLAGS before performing the following checks,
+ dnl because if the user has installed lib[]Name and not disabled its use
+ dnl via --without-lib[]Name-prefix, he wants to use it.
+ ac_save_CPPFLAGS="$CPPFLAGS"
+ AC_LIB_APPENDTOVAR([CPPFLAGS], [$INC]NAME)
+
+ AC_CACHE_CHECK([for lib[]$1], [ac_cv_lib[]Name], [
+ ac_save_LIBS="$LIBS"
+ dnl If $LIB[]NAME contains some -l options, add it to the end of LIBS,
+ dnl because these -l options might require -L options that are present in
+ dnl LIBS. -l options benefit only from the -L options listed before it.
+ dnl Otherwise, add it to the front of LIBS, because it may be a static
+ dnl library that depends on another static library that is present in LIBS.
+ dnl Static libraries benefit only from the static libraries listed after
+ dnl it.
+ case " $LIB[]NAME" in
+ *" -l"*) LIBS="$LIBS $LIB[]NAME" ;;
+ *) LIBS="$LIB[]NAME $LIBS" ;;
+ esac
+ AC_LINK_IFELSE(
+ [AC_LANG_PROGRAM([[$3]], [[$4]])],
+ [ac_cv_lib[]Name=yes],
+ [ac_cv_lib[]Name='m4_if([$5], [], [no], [[$5]])'])
+ LIBS="$ac_save_LIBS"
+ ])
+ if test "$ac_cv_lib[]Name" = yes; then
+ HAVE_LIB[]NAME=yes
+ AC_DEFINE([HAVE_LIB]NAME, 1, [Define if you have the lib][$1 library.])
+ AC_MSG_CHECKING([how to link with lib[]$1])
+ AC_MSG_RESULT([$LIB[]NAME])
+ else
+ HAVE_LIB[]NAME=no
+ dnl If $LIB[]NAME didn't lead to a usable library, we don't need
+ dnl $INC[]NAME either.
+ CPPFLAGS="$ac_save_CPPFLAGS"
+ LIB[]NAME=
+ LTLIB[]NAME=
+ LIB[]NAME[]_PREFIX=
+ fi
+ AC_SUBST([HAVE_LIB]NAME)
+ AC_SUBST([LIB]NAME)
+ AC_SUBST([LTLIB]NAME)
+ AC_SUBST([LIB]NAME[_PREFIX])
+ popdef([NAME])
+ popdef([Name])
+])
+
+dnl Determine the platform dependent parameters needed to use rpath:
+dnl acl_libext,
+dnl acl_shlibext,
+dnl acl_libname_spec,
+dnl acl_library_names_spec,
+dnl acl_hardcode_libdir_flag_spec,
+dnl acl_hardcode_libdir_separator,
+dnl acl_hardcode_direct,
+dnl acl_hardcode_minus_L.
+AC_DEFUN([AC_LIB_RPATH],
+[
+ dnl Complain if config.rpath is missing.
+ AC_REQUIRE_AUX_FILE([config.rpath])
+ AC_REQUIRE([AC_PROG_CC]) dnl we use $CC, $GCC, $LDFLAGS
+ AC_REQUIRE([AC_LIB_PROG_LD]) dnl we use $LD, $with_gnu_ld
+ AC_REQUIRE([AC_CANONICAL_HOST]) dnl we use $host
+ AC_REQUIRE([AC_CONFIG_AUX_DIR_DEFAULT]) dnl we use $ac_aux_dir
+ AC_CACHE_CHECK([for shared library run path origin], [acl_cv_rpath], [
+ CC="$CC" GCC="$GCC" LDFLAGS="$LDFLAGS" LD="$LD" with_gnu_ld="$with_gnu_ld" \
+ ${CONFIG_SHELL-/bin/sh} "$ac_aux_dir/config.rpath" "$host" > conftest.sh
+ . ./conftest.sh
+ rm -f ./conftest.sh
+ acl_cv_rpath=done
+ ])
+ wl="$acl_cv_wl"
+ acl_libext="$acl_cv_libext"
+ acl_shlibext="$acl_cv_shlibext"
+ acl_libname_spec="$acl_cv_libname_spec"
+ acl_library_names_spec="$acl_cv_library_names_spec"
+ acl_hardcode_libdir_flag_spec="$acl_cv_hardcode_libdir_flag_spec"
+ acl_hardcode_libdir_separator="$acl_cv_hardcode_libdir_separator"
+ acl_hardcode_direct="$acl_cv_hardcode_direct"
+ acl_hardcode_minus_L="$acl_cv_hardcode_minus_L"
+ dnl Determine whether the user wants rpath handling at all.
+ AC_ARG_ENABLE([rpath],
+ [ --disable-rpath do not hardcode runtime library paths],
+ :, enable_rpath=yes)
+])
+
+dnl AC_LIB_FROMPACKAGE(name, package)
+dnl declares that libname comes from the given package. The configure file
+dnl will then not have a --with-libname-prefix option but a
+dnl --with-package-prefix option. Several libraries can come from the same
+dnl package. This declaration must occur before an AC_LIB_LINKFLAGS or similar
+dnl macro call that searches for libname.
+AC_DEFUN([AC_LIB_FROMPACKAGE],
+[
+ pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-],
+ [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+ define([acl_frompackage_]NAME, [$2])
+ popdef([NAME])
+ pushdef([PACK],[$2])
+ pushdef([PACKUP],[m4_translit(PACK,[abcdefghijklmnopqrstuvwxyz./+-],
+ [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+ define([acl_libsinpackage_]PACKUP,
+ m4_ifdef([acl_libsinpackage_]PACKUP, [m4_defn([acl_libsinpackage_]PACKUP)[, ]],)[lib$1])
+ popdef([PACKUP])
+ popdef([PACK])
+])
+
+dnl AC_LIB_LINKFLAGS_BODY(name [, dependencies]) searches for libname and
+dnl the libraries corresponding to explicit and implicit dependencies.
+dnl Sets the LIB${NAME}, LTLIB${NAME} and INC${NAME} variables.
+dnl Also, sets the LIB${NAME}_PREFIX variable to nonempty if libname was found
+dnl in ${LIB${NAME}_PREFIX}/$acl_libdirstem.
+AC_DEFUN([AC_LIB_LINKFLAGS_BODY],
+[
+ AC_REQUIRE([AC_LIB_PREPARE_MULTILIB])
+ pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-],
+ [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+ pushdef([PACK],[m4_ifdef([acl_frompackage_]NAME, [acl_frompackage_]NAME, lib[$1])])
+ pushdef([PACKUP],[m4_translit(PACK,[abcdefghijklmnopqrstuvwxyz./+-],
+ [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+ pushdef([PACKLIBS],[m4_ifdef([acl_frompackage_]NAME, [acl_libsinpackage_]PACKUP, lib[$1])])
+ dnl By default, look in $includedir and $libdir.
+ use_additional=yes
+ AC_LIB_WITH_FINAL_PREFIX([
+ eval additional_includedir=\"$includedir\"
+ eval additional_libdir=\"$libdir\"
+ ])
+ AC_ARG_WITH(PACK[-prefix],
+[[ --with-]]PACK[[-prefix[=DIR] search for ]PACKLIBS[ in DIR/include and DIR/lib
+ --without-]]PACK[[-prefix don't search for ]PACKLIBS[ in includedir and libdir]],
+[
+ if test "X$withval" = "Xno"; then
+ use_additional=no
+ else
+ if test "X$withval" = "X"; then
+ AC_LIB_WITH_FINAL_PREFIX([
+ eval additional_includedir=\"$includedir\"
+ eval additional_libdir=\"$libdir\"
+ ])
+ else
+ additional_includedir="$withval/include"
+ additional_libdir="$withval/$acl_libdirstem"
+ if test "$acl_libdirstem2" != "$acl_libdirstem" \
+ && test ! -d "$withval/$acl_libdirstem"; then
+ additional_libdir="$withval/$acl_libdirstem2"
+ fi
+ fi
+ fi
+])
+ dnl Search the library and its dependencies in $additional_libdir and
+ dnl $LDFLAGS. Using breadth-first-seach.
+ LIB[]NAME=
+ LTLIB[]NAME=
+ INC[]NAME=
+ LIB[]NAME[]_PREFIX=
+ dnl HAVE_LIB${NAME} is an indicator that LIB${NAME}, LTLIB${NAME} have been
+ dnl computed. So it has to be reset here.
+ HAVE_LIB[]NAME=
+ rpathdirs=
+ ltrpathdirs=
+ names_already_handled=
+ names_next_round='$1 $2'
+ while test -n "$names_next_round"; do
+ names_this_round="$names_next_round"
+ names_next_round=
+ for name in $names_this_round; do
+ already_handled=
+ for n in $names_already_handled; do
+ if test "$n" = "$name"; then
+ already_handled=yes
+ break
+ fi
+ done
+ if test -z "$already_handled"; then
+ names_already_handled="$names_already_handled $name"
+ dnl See if it was already located by an earlier AC_LIB_LINKFLAGS
+ dnl or AC_LIB_HAVE_LINKFLAGS call.
+ uppername=`echo "$name" | sed -e 'y|abcdefghijklmnopqrstuvwxyz./+-|ABCDEFGHIJKLMNOPQRSTUVWXYZ____|'`
+ eval value=\"\$HAVE_LIB$uppername\"
+ if test -n "$value"; then
+ if test "$value" = yes; then
+ eval value=\"\$LIB$uppername\"
+ test -z "$value" || LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$value"
+ eval value=\"\$LTLIB$uppername\"
+ test -z "$value" || LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }$value"
+ else
+ dnl An earlier call to AC_LIB_HAVE_LINKFLAGS has determined
+ dnl that this library doesn't exist. So just drop it.
+ :
+ fi
+ else
+ dnl Search the library lib$name in $additional_libdir and $LDFLAGS
+ dnl and the already constructed $LIBNAME/$LTLIBNAME.
+ found_dir=
+ found_la=
+ found_so=
+ found_a=
+ eval libname=\"$acl_libname_spec\" # typically: libname=lib$name
+ if test -n "$acl_shlibext"; then
+ shrext=".$acl_shlibext" # typically: shrext=.so
+ else
+ shrext=
+ fi
+ if test $use_additional = yes; then
+ dir="$additional_libdir"
+ dnl The same code as in the loop below:
+ dnl First look for a shared library.
+ if test -n "$acl_shlibext"; then
+ if test -f "$dir/$libname$shrext"; then
+ found_dir="$dir"
+ found_so="$dir/$libname$shrext"
+ else
+ if test "$acl_library_names_spec" = '$libname$shrext$versuffix'; then
+ ver=`(cd "$dir" && \
+ for f in "$libname$shrext".*; do echo "$f"; done \
+ | sed -e "s,^$libname$shrext\\\\.,," \
+ | sort -t '.' -n -r -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 \
+ | sed 1q ) 2>/dev/null`
+ if test -n "$ver" && test -f "$dir/$libname$shrext.$ver"; then
+ found_dir="$dir"
+ found_so="$dir/$libname$shrext.$ver"
+ fi
+ else
+ eval library_names=\"$acl_library_names_spec\"
+ for f in $library_names; do
+ if test -f "$dir/$f"; then
+ found_dir="$dir"
+ found_so="$dir/$f"
+ break
+ fi
+ done
+ fi
+ fi
+ fi
+ dnl Then look for a static library.
+ if test "X$found_dir" = "X"; then
+ if test -f "$dir/$libname.$acl_libext"; then
+ found_dir="$dir"
+ found_a="$dir/$libname.$acl_libext"
+ fi
+ fi
+ if test "X$found_dir" != "X"; then
+ if test -f "$dir/$libname.la"; then
+ found_la="$dir/$libname.la"
+ fi
+ fi
+ fi
+ if test "X$found_dir" = "X"; then
+ for x in $LDFLAGS $LTLIB[]NAME; do
+ AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+ case "$x" in
+ -L*)
+ dir=`echo "X$x" | sed -e 's/^X-L//'`
+ dnl First look for a shared library.
+ if test -n "$acl_shlibext"; then
+ if test -f "$dir/$libname$shrext"; then
+ found_dir="$dir"
+ found_so="$dir/$libname$shrext"
+ else
+ if test "$acl_library_names_spec" = '$libname$shrext$versuffix'; then
+ ver=`(cd "$dir" && \
+ for f in "$libname$shrext".*; do echo "$f"; done \
+ | sed -e "s,^$libname$shrext\\\\.,," \
+ | sort -t '.' -n -r -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 \
+ | sed 1q ) 2>/dev/null`
+ if test -n "$ver" && test -f "$dir/$libname$shrext.$ver"; then
+ found_dir="$dir"
+ found_so="$dir/$libname$shrext.$ver"
+ fi
+ else
+ eval library_names=\"$acl_library_names_spec\"
+ for f in $library_names; do
+ if test -f "$dir/$f"; then
+ found_dir="$dir"
+ found_so="$dir/$f"
+ break
+ fi
+ done
+ fi
+ fi
+ fi
+ dnl Then look for a static library.
+ if test "X$found_dir" = "X"; then
+ if test -f "$dir/$libname.$acl_libext"; then
+ found_dir="$dir"
+ found_a="$dir/$libname.$acl_libext"
+ fi
+ fi
+ if test "X$found_dir" != "X"; then
+ if test -f "$dir/$libname.la"; then
+ found_la="$dir/$libname.la"
+ fi
+ fi
+ ;;
+ esac
+ if test "X$found_dir" != "X"; then
+ break
+ fi
+ done
+ fi
+ if test "X$found_dir" != "X"; then
+ dnl Found the library.
+ LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-L$found_dir -l$name"
+ if test "X$found_so" != "X"; then
+ dnl Linking with a shared library. We attempt to hardcode its
+ dnl directory into the executable's runpath, unless it's the
+ dnl standard /usr/lib.
+ if test "$enable_rpath" = no \
+ || test "X$found_dir" = "X/usr/$acl_libdirstem" \
+ || test "X$found_dir" = "X/usr/$acl_libdirstem2"; then
+ dnl No hardcoding is needed.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so"
+ else
+ dnl Use an explicit option to hardcode DIR into the resulting
+ dnl binary.
+ dnl Potentially add DIR to ltrpathdirs.
+ dnl The ltrpathdirs will be appended to $LTLIBNAME at the end.
+ haveit=
+ for x in $ltrpathdirs; do
+ if test "X$x" = "X$found_dir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ ltrpathdirs="$ltrpathdirs $found_dir"
+ fi
+ dnl The hardcoding into $LIBNAME is system dependent.
+ if test "$acl_hardcode_direct" = yes; then
+ dnl Using DIR/libNAME.so during linking hardcodes DIR into the
+ dnl resulting binary.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so"
+ else
+ if test -n "$acl_hardcode_libdir_flag_spec" && test "$acl_hardcode_minus_L" = no; then
+ dnl Use an explicit option to hardcode DIR into the resulting
+ dnl binary.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so"
+ dnl Potentially add DIR to rpathdirs.
+ dnl The rpathdirs will be appended to $LIBNAME at the end.
+ haveit=
+ for x in $rpathdirs; do
+ if test "X$x" = "X$found_dir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ rpathdirs="$rpathdirs $found_dir"
+ fi
+ else
+ dnl Rely on "-L$found_dir".
+ dnl But don't add it if it's already contained in the LDFLAGS
+ dnl or the already constructed $LIBNAME
+ haveit=
+ for x in $LDFLAGS $LIB[]NAME; do
+ AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+ if test "X$x" = "X-L$found_dir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$found_dir"
+ fi
+ if test "$acl_hardcode_minus_L" != no; then
+ dnl FIXME: Not sure whether we should use
+ dnl "-L$found_dir -l$name" or "-L$found_dir $found_so"
+ dnl here.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so"
+ else
+ dnl We cannot use $acl_hardcode_runpath_var and LD_RUN_PATH
+ dnl here, because this doesn't fit in flags passed to the
+ dnl compiler. So give up. No hardcoding. This affects only
+ dnl very old systems.
+ dnl FIXME: Not sure whether we should use
+ dnl "-L$found_dir -l$name" or "-L$found_dir $found_so"
+ dnl here.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-l$name"
+ fi
+ fi
+ fi
+ fi
+ else
+ if test "X$found_a" != "X"; then
+ dnl Linking with a static library.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_a"
+ else
+ dnl We shouldn't come here, but anyway it's good to have a
+ dnl fallback.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$found_dir -l$name"
+ fi
+ fi
+ dnl Assume the include files are nearby.
+ additional_includedir=
+ case "$found_dir" in
+ */$acl_libdirstem | */$acl_libdirstem/)
+ basedir=`echo "X$found_dir" | sed -e 's,^X,,' -e "s,/$acl_libdirstem/"'*$,,'`
+ if test "$name" = '$1'; then
+ LIB[]NAME[]_PREFIX="$basedir"
+ fi
+ additional_includedir="$basedir/include"
+ ;;
+ */$acl_libdirstem2 | */$acl_libdirstem2/)
+ basedir=`echo "X$found_dir" | sed -e 's,^X,,' -e "s,/$acl_libdirstem2/"'*$,,'`
+ if test "$name" = '$1'; then
+ LIB[]NAME[]_PREFIX="$basedir"
+ fi
+ additional_includedir="$basedir/include"
+ ;;
+ esac
+ if test "X$additional_includedir" != "X"; then
+ dnl Potentially add $additional_includedir to $INCNAME.
+ dnl But don't add it
+ dnl 1. if it's the standard /usr/include,
+ dnl 2. if it's /usr/local/include and we are using GCC on Linux,
+ dnl 3. if it's already present in $CPPFLAGS or the already
+ dnl constructed $INCNAME,
+ dnl 4. if it doesn't exist as a directory.
+ if test "X$additional_includedir" != "X/usr/include"; then
+ haveit=
+ if test "X$additional_includedir" = "X/usr/local/include"; then
+ if test -n "$GCC"; then
+ case $host_os in
+ linux* | gnu* | k*bsd*-gnu) haveit=yes;;
+ esac
+ fi
+ fi
+ if test -z "$haveit"; then
+ for x in $CPPFLAGS $INC[]NAME; do
+ AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+ if test "X$x" = "X-I$additional_includedir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ if test -d "$additional_includedir"; then
+ dnl Really add $additional_includedir to $INCNAME.
+ INC[]NAME="${INC[]NAME}${INC[]NAME:+ }-I$additional_includedir"
+ fi
+ fi
+ fi
+ fi
+ fi
+ dnl Look for dependencies.
+ if test -n "$found_la"; then
+ dnl Read the .la file. It defines the variables
+ dnl dlname, library_names, old_library, dependency_libs, current,
+ dnl age, revision, installed, dlopen, dlpreopen, libdir.
+ save_libdir="$libdir"
+ case "$found_la" in
+ */* | *\\*) . "$found_la" ;;
+ *) . "./$found_la" ;;
+ esac
+ libdir="$save_libdir"
+ dnl We use only dependency_libs.
+ for dep in $dependency_libs; do
+ case "$dep" in
+ -L*)
+ additional_libdir=`echo "X$dep" | sed -e 's/^X-L//'`
+ dnl Potentially add $additional_libdir to $LIBNAME and $LTLIBNAME.
+ dnl But don't add it
+ dnl 1. if it's the standard /usr/lib,
+ dnl 2. if it's /usr/local/lib and we are using GCC on Linux,
+ dnl 3. if it's already present in $LDFLAGS or the already
+ dnl constructed $LIBNAME,
+ dnl 4. if it doesn't exist as a directory.
+ if test "X$additional_libdir" != "X/usr/$acl_libdirstem" \
+ && test "X$additional_libdir" != "X/usr/$acl_libdirstem2"; then
+ haveit=
+ if test "X$additional_libdir" = "X/usr/local/$acl_libdirstem" \
+ || test "X$additional_libdir" = "X/usr/local/$acl_libdirstem2"; then
+ if test -n "$GCC"; then
+ case $host_os in
+ linux* | gnu* | k*bsd*-gnu) haveit=yes;;
+ esac
+ fi
+ fi
+ if test -z "$haveit"; then
+ haveit=
+ for x in $LDFLAGS $LIB[]NAME; do
+ AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+ if test "X$x" = "X-L$additional_libdir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ if test -d "$additional_libdir"; then
+ dnl Really add $additional_libdir to $LIBNAME.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$additional_libdir"
+ fi
+ fi
+ haveit=
+ for x in $LDFLAGS $LTLIB[]NAME; do
+ AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+ if test "X$x" = "X-L$additional_libdir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ if test -d "$additional_libdir"; then
+ dnl Really add $additional_libdir to $LTLIBNAME.
+ LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-L$additional_libdir"
+ fi
+ fi
+ fi
+ fi
+ ;;
+ -R*)
+ dir=`echo "X$dep" | sed -e 's/^X-R//'`
+ if test "$enable_rpath" != no; then
+ dnl Potentially add DIR to rpathdirs.
+ dnl The rpathdirs will be appended to $LIBNAME at the end.
+ haveit=
+ for x in $rpathdirs; do
+ if test "X$x" = "X$dir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ rpathdirs="$rpathdirs $dir"
+ fi
+ dnl Potentially add DIR to ltrpathdirs.
+ dnl The ltrpathdirs will be appended to $LTLIBNAME at the end.
+ haveit=
+ for x in $ltrpathdirs; do
+ if test "X$x" = "X$dir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ ltrpathdirs="$ltrpathdirs $dir"
+ fi
+ fi
+ ;;
+ -l*)
+ dnl Handle this in the next round.
+ names_next_round="$names_next_round "`echo "X$dep" | sed -e 's/^X-l//'`
+ ;;
+ *.la)
+ dnl Handle this in the next round. Throw away the .la's
+ dnl directory; it is already contained in a preceding -L
+ dnl option.
+ names_next_round="$names_next_round "`echo "X$dep" | sed -e 's,^X.*/,,' -e 's,^lib,,' -e 's,\.la$,,'`
+ ;;
+ *)
+ dnl Most likely an immediate library name.
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$dep"
+ LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }$dep"
+ ;;
+ esac
+ done
+ fi
+ else
+ dnl Didn't find the library; assume it is in the system directories
+ dnl known to the linker and runtime loader. (All the system
+ dnl directories known to the linker should also be known to the
+ dnl runtime loader, otherwise the system is severely misconfigured.)
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-l$name"
+ LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-l$name"
+ fi
+ fi
+ fi
+ done
+ done
+ if test "X$rpathdirs" != "X"; then
+ if test -n "$acl_hardcode_libdir_separator"; then
+ dnl Weird platform: only the last -rpath option counts, the user must
+ dnl pass all path elements in one option. We can arrange that for a
+ dnl single library, but not when more than one $LIBNAMEs are used.
+ alldirs=
+ for found_dir in $rpathdirs; do
+ alldirs="${alldirs}${alldirs:+$acl_hardcode_libdir_separator}$found_dir"
+ done
+ dnl Note: acl_hardcode_libdir_flag_spec uses $libdir and $wl.
+ acl_save_libdir="$libdir"
+ libdir="$alldirs"
+ eval flag=\"$acl_hardcode_libdir_flag_spec\"
+ libdir="$acl_save_libdir"
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$flag"
+ else
+ dnl The -rpath options are cumulative.
+ for found_dir in $rpathdirs; do
+ acl_save_libdir="$libdir"
+ libdir="$found_dir"
+ eval flag=\"$acl_hardcode_libdir_flag_spec\"
+ libdir="$acl_save_libdir"
+ LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$flag"
+ done
+ fi
+ fi
+ if test "X$ltrpathdirs" != "X"; then
+ dnl When using libtool, the option that works for both libraries and
+ dnl executables is -R. The -R options are cumulative.
+ for found_dir in $ltrpathdirs; do
+ LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-R$found_dir"
+ done
+ fi
+ popdef([PACKLIBS])
+ popdef([PACKUP])
+ popdef([PACK])
+ popdef([NAME])
+])
+
+dnl AC_LIB_APPENDTOVAR(VAR, CONTENTS) appends the elements of CONTENTS to VAR,
+dnl unless already present in VAR.
+dnl Works only for CPPFLAGS, not for LIB* variables because that sometimes
+dnl contains two or three consecutive elements that belong together.
+AC_DEFUN([AC_LIB_APPENDTOVAR],
+[
+ for element in [$2]; do
+ haveit=
+ for x in $[$1]; do
+ AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+ if test "X$x" = "X$element"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ [$1]="${[$1]}${[$1]:+ }$element"
+ fi
+ done
+])
+
+dnl For those cases where a variable contains several -L and -l options
+dnl referring to unknown libraries and directories, this macro determines the
+dnl necessary additional linker options for the runtime path.
+dnl AC_LIB_LINKFLAGS_FROM_LIBS([LDADDVAR], [LIBSVALUE], [USE-LIBTOOL])
+dnl sets LDADDVAR to linker options needed together with LIBSVALUE.
+dnl If USE-LIBTOOL evaluates to non-empty, linking with libtool is assumed,
+dnl otherwise linking without libtool is assumed.
+AC_DEFUN([AC_LIB_LINKFLAGS_FROM_LIBS],
+[
+ AC_REQUIRE([AC_LIB_RPATH])
+ AC_REQUIRE([AC_LIB_PREPARE_MULTILIB])
+ $1=
+ if test "$enable_rpath" != no; then
+ if test -n "$acl_hardcode_libdir_flag_spec" && test "$acl_hardcode_minus_L" = no; then
+ dnl Use an explicit option to hardcode directories into the resulting
+ dnl binary.
+ rpathdirs=
+ next=
+ for opt in $2; do
+ if test -n "$next"; then
+ dir="$next"
+ dnl No need to hardcode the standard /usr/lib.
+ if test "X$dir" != "X/usr/$acl_libdirstem" \
+ && test "X$dir" != "X/usr/$acl_libdirstem2"; then
+ rpathdirs="$rpathdirs $dir"
+ fi
+ next=
+ else
+ case $opt in
+ -L) next=yes ;;
+ -L*) dir=`echo "X$opt" | sed -e 's,^X-L,,'`
+ dnl No need to hardcode the standard /usr/lib.
+ if test "X$dir" != "X/usr/$acl_libdirstem" \
+ && test "X$dir" != "X/usr/$acl_libdirstem2"; then
+ rpathdirs="$rpathdirs $dir"
+ fi
+ next= ;;
+ *) next= ;;
+ esac
+ fi
+ done
+ if test "X$rpathdirs" != "X"; then
+ if test -n ""$3""; then
+ dnl libtool is used for linking. Use -R options.
+ for dir in $rpathdirs; do
+ $1="${$1}${$1:+ }-R$dir"
+ done
+ else
+ dnl The linker is used for linking directly.
+ if test -n "$acl_hardcode_libdir_separator"; then
+ dnl Weird platform: only the last -rpath option counts, the user
+ dnl must pass all path elements in one option.
+ alldirs=
+ for dir in $rpathdirs; do
+ alldirs="${alldirs}${alldirs:+$acl_hardcode_libdir_separator}$dir"
+ done
+ acl_save_libdir="$libdir"
+ libdir="$alldirs"
+ eval flag=\"$acl_hardcode_libdir_flag_spec\"
+ libdir="$acl_save_libdir"
+ $1="$flag"
+ else
+ dnl The -rpath options are cumulative.
+ for dir in $rpathdirs; do
+ acl_save_libdir="$libdir"
+ libdir="$dir"
+ eval flag=\"$acl_hardcode_libdir_flag_spec\"
+ libdir="$acl_save_libdir"
+ $1="${$1}${$1:+ }$flag"
+ done
+ fi
+ fi
+ fi
+ fi
+ fi
+ AC_SUBST([$1])
+])
--- /dev/null
+# lib-prefix.m4 serial 14
+dnl Copyright (C) 2001-2005, 2008-2019 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+dnl From Bruno Haible.
+
+dnl AC_LIB_PREFIX adds to the CPPFLAGS and LDFLAGS the flags that are needed
+dnl to access previously installed libraries. The basic assumption is that
+dnl a user will want packages to use other packages he previously installed
+dnl with the same --prefix option.
+dnl This macro is not needed if only AC_LIB_LINKFLAGS is used to locate
+dnl libraries, but is otherwise very convenient.
+AC_DEFUN([AC_LIB_PREFIX],
+[
+ AC_BEFORE([$0], [AC_LIB_LINKFLAGS])
+ AC_REQUIRE([AC_PROG_CC])
+ AC_REQUIRE([AC_CANONICAL_HOST])
+ AC_REQUIRE([AC_LIB_PREPARE_MULTILIB])
+ AC_REQUIRE([AC_LIB_PREPARE_PREFIX])
+ dnl By default, look in $includedir and $libdir.
+ use_additional=yes
+ AC_LIB_WITH_FINAL_PREFIX([
+ eval additional_includedir=\"$includedir\"
+ eval additional_libdir=\"$libdir\"
+ ])
+ AC_ARG_WITH([lib-prefix],
+[[ --with-lib-prefix[=DIR] search for libraries in DIR/include and DIR/lib
+ --without-lib-prefix don't search for libraries in includedir and libdir]],
+[
+ if test "X$withval" = "Xno"; then
+ use_additional=no
+ else
+ if test "X$withval" = "X"; then
+ AC_LIB_WITH_FINAL_PREFIX([
+ eval additional_includedir=\"$includedir\"
+ eval additional_libdir=\"$libdir\"
+ ])
+ else
+ additional_includedir="$withval/include"
+ additional_libdir="$withval/$acl_libdirstem"
+ fi
+ fi
+])
+ if test $use_additional = yes; then
+ dnl Potentially add $additional_includedir to $CPPFLAGS.
+ dnl But don't add it
+ dnl 1. if it's the standard /usr/include,
+ dnl 2. if it's already present in $CPPFLAGS,
+ dnl 3. if it's /usr/local/include and we are using GCC on Linux,
+ dnl 4. if it doesn't exist as a directory.
+ if test "X$additional_includedir" != "X/usr/include"; then
+ haveit=
+ for x in $CPPFLAGS; do
+ AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+ if test "X$x" = "X-I$additional_includedir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ if test "X$additional_includedir" = "X/usr/local/include"; then
+ if test -n "$GCC"; then
+ case $host_os in
+ linux* | gnu* | k*bsd*-gnu) haveit=yes;;
+ esac
+ fi
+ fi
+ if test -z "$haveit"; then
+ if test -d "$additional_includedir"; then
+ dnl Really add $additional_includedir to $CPPFLAGS.
+ CPPFLAGS="${CPPFLAGS}${CPPFLAGS:+ }-I$additional_includedir"
+ fi
+ fi
+ fi
+ fi
+ dnl Potentially add $additional_libdir to $LDFLAGS.
+ dnl But don't add it
+ dnl 1. if it's the standard /usr/lib,
+ dnl 2. if it's already present in $LDFLAGS,
+ dnl 3. if it's /usr/local/lib and we are using GCC on Linux,
+ dnl 4. if it doesn't exist as a directory.
+ if test "X$additional_libdir" != "X/usr/$acl_libdirstem"; then
+ haveit=
+ for x in $LDFLAGS; do
+ AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+ if test "X$x" = "X-L$additional_libdir"; then
+ haveit=yes
+ break
+ fi
+ done
+ if test -z "$haveit"; then
+ if test "X$additional_libdir" = "X/usr/local/$acl_libdirstem"; then
+ if test -n "$GCC"; then
+ case $host_os in
+ linux*) haveit=yes;;
+ esac
+ fi
+ fi
+ if test -z "$haveit"; then
+ if test -d "$additional_libdir"; then
+ dnl Really add $additional_libdir to $LDFLAGS.
+ LDFLAGS="${LDFLAGS}${LDFLAGS:+ }-L$additional_libdir"
+ fi
+ fi
+ fi
+ fi
+ fi
+])
+
+dnl AC_LIB_PREPARE_PREFIX creates variables acl_final_prefix,
+dnl acl_final_exec_prefix, containing the values to which $prefix and
+dnl $exec_prefix will expand at the end of the configure script.
+AC_DEFUN([AC_LIB_PREPARE_PREFIX],
+[
+ dnl Unfortunately, prefix and exec_prefix get only finally determined
+ dnl at the end of configure.
+ if test "X$prefix" = "XNONE"; then
+ acl_final_prefix="$ac_default_prefix"
+ else
+ acl_final_prefix="$prefix"
+ fi
+ if test "X$exec_prefix" = "XNONE"; then
+ acl_final_exec_prefix='${prefix}'
+ else
+ acl_final_exec_prefix="$exec_prefix"
+ fi
+ acl_save_prefix="$prefix"
+ prefix="$acl_final_prefix"
+ eval acl_final_exec_prefix=\"$acl_final_exec_prefix\"
+ prefix="$acl_save_prefix"
+])
+
+dnl AC_LIB_WITH_FINAL_PREFIX([statement]) evaluates statement, with the
+dnl variables prefix and exec_prefix bound to the values they will have
+dnl at the end of the configure script.
+AC_DEFUN([AC_LIB_WITH_FINAL_PREFIX],
+[
+ acl_save_prefix="$prefix"
+ prefix="$acl_final_prefix"
+ acl_save_exec_prefix="$exec_prefix"
+ exec_prefix="$acl_final_exec_prefix"
+ $1
+ exec_prefix="$acl_save_exec_prefix"
+ prefix="$acl_save_prefix"
+])
+
+dnl AC_LIB_PREPARE_MULTILIB creates
+dnl - a variable acl_libdirstem, containing the basename of the libdir, either
+dnl "lib" or "lib64" or "lib/64",
+dnl - a variable acl_libdirstem2, as a secondary possible value for
+dnl acl_libdirstem, either the same as acl_libdirstem or "lib/sparcv9" or
+dnl "lib/amd64".
+AC_DEFUN([AC_LIB_PREPARE_MULTILIB],
+[
+ dnl There is no formal standard regarding lib and lib64.
+ dnl On glibc systems, the current practice is that on a system supporting
+ dnl 32-bit and 64-bit instruction sets or ABIs, 64-bit libraries go under
+ dnl $prefix/lib64 and 32-bit libraries go under $prefix/lib. We determine
+ dnl the compiler's default mode by looking at the compiler's library search
+ dnl path. If at least one of its elements ends in /lib64 or points to a
+ dnl directory whose absolute pathname ends in /lib64, we assume a 64-bit ABI.
+ dnl Otherwise we use the default, namely "lib".
+ dnl On Solaris systems, the current practice is that on a system supporting
+ dnl 32-bit and 64-bit instruction sets or ABIs, 64-bit libraries go under
+ dnl $prefix/lib/64 (which is a symlink to either $prefix/lib/sparcv9 or
+ dnl $prefix/lib/amd64) and 32-bit libraries go under $prefix/lib.
+ AC_REQUIRE([AC_CANONICAL_HOST])
+ AC_REQUIRE([gl_HOST_CPU_C_ABI_32BIT])
+
+ case "$host_os" in
+ solaris*)
+ AC_CACHE_CHECK([for 64-bit host], [gl_cv_solaris_64bit],
+ [AC_COMPILE_IFELSE(
+ [AC_LANG_SOURCE(
+ [[#ifdef _LP64
+ int ok;
+ #else
+ error fail
+ #endif
+ ]])],
+ [gl_cv_solaris_64bit=yes],
+ [gl_cv_solaris_64bit=no])
+ ]);;
+ esac
+
+ dnl Allow the user to override the result by setting acl_cv_libdirstems.
+ AC_CACHE_CHECK([for the common suffixes of directories in the library search path],
+ [acl_cv_libdirstems],
+ [acl_libdirstem=lib
+ acl_libdirstem2=
+ case "$host_os" in
+ solaris*)
+ dnl See Solaris 10 Software Developer Collection > Solaris 64-bit Developer's Guide > The Development Environment
+ dnl <https://docs.oracle.com/cd/E19253-01/816-5138/dev-env/index.html>.
+ dnl "Portable Makefiles should refer to any library directories using the 64 symbolic link."
+ dnl But we want to recognize the sparcv9 or amd64 subdirectory also if the
+ dnl symlink is missing, so we set acl_libdirstem2 too.
+ if test $gl_cv_solaris_64bit = yes; then
+ acl_libdirstem=lib/64
+ case "$host_cpu" in
+ sparc*) acl_libdirstem2=lib/sparcv9 ;;
+ i*86 | x86_64) acl_libdirstem2=lib/amd64 ;;
+ esac
+ fi
+ ;;
+ *)
+ dnl If $CC generates code for a 32-bit ABI, the libraries are
+ dnl surely under $prefix/lib, not $prefix/lib64.
+ if test "$HOST_CPU_C_ABI_32BIT" != yes; then
+ dnl The result is a property of the system. However, non-system
+ dnl compilers sometimes have odd library search paths. Therefore
+ dnl prefer asking /usr/bin/gcc, if available, rather than $CC.
+ searchpath=`(if test -f /usr/bin/gcc \
+ && LC_ALL=C /usr/bin/gcc -print-search-dirs >/dev/null 2>/dev/null; then \
+ LC_ALL=C /usr/bin/gcc -print-search-dirs; \
+ else \
+ LC_ALL=C $CC -print-search-dirs; \
+ fi) 2>/dev/null \
+ | sed -n -e 's,^libraries: ,,p' | sed -e 's,^=,,'`
+ if test -n "$searchpath"; then
+ acl_save_IFS="${IFS= }"; IFS=":"
+ for searchdir in $searchpath; do
+ if test -d "$searchdir"; then
+ case "$searchdir" in
+ */lib64/ | */lib64 ) acl_libdirstem=lib64 ;;
+ */../ | */.. )
+ # Better ignore directories of this form. They are misleading.
+ ;;
+ *) searchdir=`cd "$searchdir" && pwd`
+ case "$searchdir" in
+ */lib64 ) acl_libdirstem=lib64 ;;
+ esac ;;
+ esac
+ fi
+ done
+ IFS="$acl_save_IFS"
+ fi
+ fi
+ ;;
+ esac
+ test -n "$acl_libdirstem2" || acl_libdirstem2="$acl_libdirstem"
+ acl_cv_libdirstems="$acl_libdirstem,$acl_libdirstem2"
+ ])
+ # Decompose acl_cv_libdirstems into acl_libdirstem and acl_libdirstem2.
+ acl_libdirstem=`echo "$acl_cv_libdirstems" | sed -e 's/,.*//'`
+ acl_libdirstem2=`echo "$acl_cv_libdirstems" | sed -e '/,/s/.*,//'`
+])
--- /dev/null
+## Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+dist_man_MANS = \
+ mu-add.1 \
+ mu-bookmarks.5 \
+ mu-cfind.1 \
+ mu-easy.1 \
+ mu-extract.1 \
+ mu-fields.1 \
+ mu-find.1 \
+ mu-help.1 \
+ mu-index.1 \
+ mu-info.1 \
+ mu-init.1 \
+ mu-mkdir.1 \
+ mu-query.7 \
+ mu-remove.1 \
+ mu-server.1 \
+ mu-script.1 \
+ mu-verify.1 \
+ mu-view.1 \
+ mu.1
--- /dev/null
+## 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.
+
+install_man(
+ ['mu.1',
+ 'mu-add.1',
+ 'mu-bookmarks.5',
+ 'mu-cfind.1',
+ 'mu-easy.1',
+ 'mu-extract.1',
+ 'mu-fields.1',
+ 'mu-find.1',
+ 'mu-help.1',
+ 'mu-index.1',
+ 'mu-info.1',
+ 'mu-init.1',
+ 'mu-mkdir.1',
+ 'mu-query.7',
+ 'mu-remove.1',
+ 'mu-script.1',
+ 'mu-server.1',
+ 'mu-verify.1',
+ 'mu-view.1'])
--- /dev/null
+.TH MU ADD 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+mu add\- add one or more messages to the database
+
+.SH SYNOPSIS
+
+.B mu add <file> [<files>]
+
+.SH DESCRIPTION
+
+\fBmu add\fR is the command to add specific message files to the
+database. Each file must be specified with an absolute path.
+
+.SH OPTIONS
+
+\fBmu add\fR does not have its own options, but the general options for
+determining the location of the database (\fI--muhome\fR) are available. See
+\fBmu-index\fR(1) for more information.
+
+.SH RETURN VALUE
+
+\fBmu add\fR returns 0 upon success; in general, the following error codes are
+returned:
+
+.nf
+| code | meaning |
+|------+-----------------------------------|
+| 0 | ok |
+| 1 | general error |
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR mu-index (1),
+.BR mu-remove (1)
--- /dev/null
+.TH MU-BOOKMARKS 5 "July 2019" "User Manuals"
+
+.SH NAME
+
+bookmarks \- file with bookmarks (shortcuts) for mu search expressions
+
+.SH DESCRIPTION
+
+Bookmarks are named shortcuts for search queries. They allow using a convenient
+name for often-used queries. The bookmarks are also visible as shortcuts in the
+mu experimental user interfaces, \fImug\fR and \fImug2\fR.
+
+The bookmarks file is read from \fI<muhome>/bookmarks\fR. On Unix this would
+typically be w be \fI~/.config/mu/bookmarks\fR, but this can be influenced using
+the \fB\-\-muhome\fR parameter for \fBmu-find\fR(1) and \fBmug\fR(1).
+
+The bookmarks file is a typical key=value \fB.ini\fR-file, which is best shown
+by means of an example:
+
+.nf
+ [mu]
+ inbox=maildir:/inbox # inbox
+ oldhat=maildir:/archive subject:hat # archived with subject containing 'hat'
+.fi
+
+The \fB[mu]\fR group header is required.
+
+For practical uses of bookmarks, see \fBmu-find\fR(1).
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1), mu-find (1)
--- /dev/null
+.TH MU CFIND 1 "April 2019" "User Manuals"
+
+.SH NAME
+
+\fBmu cfind\fR is the \fBmu\fR command to find contacts in the \fBmu\fR
+database and export them for use in other programs.
+
+.SH SYNOPSIS
+
+.B mu cfind [options] [<pattern>]
+
+.SH DESCRIPTION
+
+\fBmu cfind\fR is the \fBmu\fR command for finding \fIcontacts\fR (name and
+e-mail address of people who were either an e-mail's sender or
+receiver). There are different output formats available, for importing the
+contacts into other programs.
+
+.SH SEARCHING CONTACTS
+
+When you index your messages (see \fBmu index\fR), \fBmu\fR creates a list of
+unique e-mail addresses found and the accompanying name, and caches this
+list. In case the same e-mail address is used with different names, the most
+recent non-empty name is used.
+
+\fBmu cfind\fR starts a search for contacts that match a \fIregular
+expression\fR. For example:
+
+.nf
+ $ mu cfind '@gmail\.com'
+.fi
+
+would find all contacts with a gmail-address, while
+
+.nf
+ $ mu cfind Mary
+.fi
+
+lists all contacts with Mary in either name or e-mail address.
+
+If you do not specify a search expression, \fBmu cfind\fR returns the full list
+of contacts. Note, \fBmu cfind\fR uses a cache with the e-mail information,
+which is populated during the indexing process.
+
+The regular expressions are Perl-compatible (as per the PCRE-library used by
+GRegex).
+
+.SH OPTIONS
+
+.TP
+\fB\-\-format\fR=\fIplain|mutt-alias|mutt-ab|wl|org-contact|bbdb|csv\fR
+sets the output format to the given value. The following are available:
+
+.nf
+| --format= | description |
+|-------------+-----------------------------------|
+| plain | default, simple list |
+| mutt-alias | mutt alias-format |
+| mutt-ab | mutt external address book format |
+| wl | wanderlust addressbook format |
+| org-contact | org-mode org-contact format |
+| bbdb | BBDB format |
+| csv | comma-separated values (*) |
+.fi
+
+
+(*) CSV is not fully standardized, but \fBmu cfind\fR follows some common
+practices: any double-quote is replaced by a double-double quote (thus, "hello"
+become ""hello"", and fields with commas are put in double-quotes. Normally,
+this should only apply to name fields.
+
+.TP
+\fB\-\-personal\fR only show addresses seen in messages where one of 'my' e-mail
+addresses was seen in one of the address fields; this is to exclude addresses
+only seen in mailing-list messages. See the \fB\-\-my-address\fR parameter in
+\fBmu index\fR.
+
+.TP
+\fB\-\-after=\fR\fI<timestamp>\fR only show addresses last seen after
+\fI<timestamp>\fR. \fI<timestamp>\fR is a UNIX \fBtime_t\fR value, the number of
+seconds since 1970-01-01 (in UTC).
+
+From the command line, you can use the \fBdate\fR command to get this value. For
+example, only consider addresses last seen after 2009-06-01, you could specify
+.nf
+ --after=`date +%s --date='2009-06-01'`
+.fi
+
+.SH RETURN VALUE
+
+\fBmu cfind\fR returns 0 upon successful completion -- that is, at least one
+contact was found. Anything else leads to a non-zero return value:
+
+.nf
+| code | meaning |
+|------+--------------------------------|
+| 0 | ok |
+| 1 | general error |
+| 2 | no matches (for 'mu cfind') |
+.fi
+
+.SH INTEGRATION WITH MUTT
+
+You can use \fBmu cfind\fR as an external address book server for \fBmutt\fR.
+For this to work, add the following to your \fImuttrc\fR:
+
+.nf
+set query_command = "mu cfind --format=mutt-ab '%s'"
+.fi
+
+Now, in mutt, you can search for e-mail addresses using the \fBquery\fR-command,
+which is (by default) accessible by pressing \fBQ\fR.
+
+.SH ENCODING
+
+\fBmu cfind\fR output is encoded according to the current locale except for
+\fI--format=bbdb\fR. This is hard-coded to UTF-8, and as such specified in the
+output-file, so emacs/bbdb can handle things correctly, without guessing.
+
+.SH BUGS
+
+Please report bugs if you find them at \fBhttps://github.com/djcb/mu/issues\fR.
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR mu-index (1),
+.BR mu-find (1),
+.BR pcrepattern(3)
--- /dev/null
+.TH MU-EASY 1 "February 2020" "User Manuals"
+
+.SH NAME
+
+mu easy \- a quick introduction to mu
+
+.SH DESCRIPTION
+
+\fBmu\fR is a set of tools for dealing with e-mail messages in Maildirs. There
+are many options, which are all described in the man pages for the various
+sub-commands. This man pages jumps over all of the details and gives examples of
+some common use cases. If the use cases described here do not precisely do what
+you want, please check the more extensive information in the man page about the
+sub-command you are using -- for example, the \fBmu-index\fR(1) or
+\fBmu-find\fR(1) man pages.
+
+\fBNOTE\fR: the \fBindex\fR command (and therefore, the ones that depend on
+that, such as \fBfind\fR), require that you store your mail in the
+Maildir-format. If you don't do so, you can still use the other commands, but
+you won't be able to index/search your mail.
+
+By default, \fBmu\fR uses colorized output when it thinks your terminal is
+capable of doing so. If you don't like color, you can use the \fB--nocolor\fR
+command-line option, or set either the \fBMU_NOCOLOR\fR or the \fBNO_COLOR\fR
+environment variable to non-empty.
+
+.SH SETTING THINGS UP
+
+The first time you run the mu commands, you need to initialize it. This is done
+with the \fBinit\fR command.
+
+.nf
+ \fB$ mu init\fR
+.fi
+
+This uses the defaults (see \fBmu-init(1)\fR for details on how to change that).
+
+
+.SH INDEXING YOUR E-MAIL
+
+Before you can search e-mails, you'll first need to index them:
+
+.nf
+ \fB$ mu index\fR
+.fi
+
+The process can take a few minutes, depending on the amount of mail
+you have, the speed of your computer, hard drive etc. Usually,
+indexing should be able to reach a speed of a few hundred messages per
+second.
+
+\fBmu index\fR guesses the top-level Maildir to do its job; if it guesses wrong,
+you can use the \fI--maildir\fR option to specify the top-level directory that
+should be processed. See the \fBmu-index\fR(1) man page for more details.
+
+Normally, \fBmu index\fR visits all the directories under the top-level Maildir;
+however, you can exclude certain directories (say, the 'trash' or 'spam'
+folders) by creating a file called \fI.noindex\fR in the directory. When
+\fBmu\fR sees such a file, it will exclude this directory and its
+sub-directories from indexing. Also see \fB.noupdate\fR in the \fBmu-index\fR(1)
+manpage.
+
+.SH SEARCHING YOUR E-MAIL
+
+After you have indexed your mail, you can start searching it. By default, the
+search results are printed on standard output. Alternatively, the output can
+take the form of Maildir with symbolic links to the found messages. This enables
+integration with e-mail clients; see the \fBmu-find\fR(1) man page for details,
+the syntax of the search parameters and so on. Here, we just give some examples
+for common cases.
+
+You can use the \fBmu fields\fR command to get information about all possible
+fields and flags.
+
+First, let's search for all messages sent to Julius (Caesar) regarding
+fruit:
+
+.nf
+\fB$ mu find t:julius fruit\fR
+.fi
+
+This should return something like:
+
+.nf
+ 2008-07-31T21:57:25 EEST John Milton <jm@example.com> Fere libenter homines id quod volunt credunt
+.fi
+
+This means there is a message to 'julius' with 'fruit' somewhere in the message.
+In this case, it's a message from John Milton. Note that the date format depends
+on your the language/locale you are using.
+
+How do we know that the message was sent to Julius Caesar? Well, it's not
+visible from the results above, because the default fields that are shown are
+date/sender/subject. However, we can change this using the \fI--fields\fR
+parameter (try \fBmu fields\fR to see all the details):
+
+.nf
+ \fB$ mu find --fields="t s" t:julius fruit\fR
+.fi
+
+In other words, display the 'To:'-field (t) and the subject (s). This should
+return something like:
+.nf
+ Julius Caesar <jc@example.com> Fere libenter homines id quod volunt credunt
+.fi
+
+This is the same message found before, only with some different fields
+displayed.
+
+By default, \fBmu\fR uses the logical AND for the search parameters -- that
+is, it displays messages that match all the parameters. However, we can use
+logical OR as well:
+
+.nf
+ \fB$ mu find t:julius OR f:socrates\fR
+.fi
+
+In other words, display messages that are either sent to Julius Caesar
+\fBor\fR are from Socrates. This could return something like:
+
+.nf
+ 2008-07-31T21:57:25 EEST Socrates <soc@example.com> cool stuff
+ 2008-07-31T21:57:25 EEST John Milton <jm@example.com> Fere libenter homines id quod volunt credunt
+.fi
+
+What if we want to see some of the body of the message? You can get
+a 'summary' of the first lines of the message using the \fI--summary-len\fR
+option, which will 'summarize' the first \fIn\fR lines of the message:
+
+.nf
+ \fB$ mu find --summary-len=3 napoleon m:/archive\fR
+.fi
+
+.nf
+ 1970-01-01T02:00:00 EET Napoleon Bonaparte <nb@example.com> rock on dude
+ Summary: Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le
+ trois-mâts le Pharaon, venant de Smyrne, Trieste et Naples. Comme
+ d'habitude, un pilote côtier partit aussitôt du port, rasa le château
+.fi
+
+The summary consists of the first n lines of the message with all superfluous
+whitespace removed.
+
+Also note the \fBm:/archive\fR parameter in the query. This means that we only
+match messages in a maildir called '/archive'.
+
+.SH MORE QUERIES
+
+Let's list a few more queries that may be interesting; please note that
+searches for message flags, priority and date ranges are only available in mu
+version 0.9 or later.
+
+Get all important messages which are signed:
+.nf
+ \fB$ mu find flag:signed prio:high \fR
+.fi
+
+Get all messages from Jim without an attachment:
+.nf
+ \fB$ mu find from:jim AND NOT flag:attach\fR
+.fi
+
+Get all messages where Jack is in one of the contact fields:
+.nf
+ \fB$ mu find contact:jack\fR
+.fi
+This uses the special contact: pseudo-field which matches (\fBfrom\fR,
+\fBto\fR, \fBcc\fR and \fBbcc\fR).
+
+Get all messages in the Sent Items folder about yoghurt:
+.nf
+ \fB$mu find maildir:'/Sent Items' yoghurt\fR
+.fi
+Note how we need to quote search terms that include spaces.
+
+
+Get all unread messages where the subject mentions Ångström:
+.nf
+ \fB$ mu find subject:Ångström flag:unread\fR
+.fi
+which is equivalent to:
+.nf
+ \fB$ mu find subject:angstrom flag:unread\fR
+.fi
+because does mu is case-insensitive and accent-insensitive.
+
+Get all unread messages between March 2002 and August 2003 about some bird (or
+a Swedish rock band):
+.nf
+ \fB$ mu find date:20020301..20030831 nightingale flag:unread\fR
+.fi
+
+Get all messages received today:
+.nf
+ \fB$ mu find date:today..now\fR
+.fi
+
+Get all messages we got in the last two weeks about emacs:
+.nf
+ \fB$ mu find date:2w..now emacs\fR
+.fi
+
+Another powerful feature (since 0.9.6) are wildcard searches, where you can
+search for the last \fIn\fR characters in a word. For example, you can search
+for:
+.nf
+ \fB$ mu find 'subject:soc*'\fR
+.fi
+and get mails about soccer, Socrates, society, and so on. Note, it's important
+to quote the search query, otherwise the shell will interpret
+the '*'.
+
+You can also search for messages with a certain attachment using their
+filename, for example:
+
+.nf
+ \fB$ mu find 'file:pic*'\fR
+.fi
+will get you all messages with an attachment starting with 'pic'.
+
+If you want to find attachments with a certain MIME-type, you can use the
+following:
+
+Get all messages with PDF attachments:
+.nf
+ \fB$ mu find mime:application/pdf\fR
+.fi
+
+or even:
+
+Get all messages with image attachments:
+.nf
+ \fB$ mu find 'mime:image/*'\fR
+.fi
+
+
+Note that (1) the '*' wildcard can only be used as the rightmost thing in a
+search query, and (2) that you need to quote the search term, because
+otherwise your shell will interpret the '*' (expanding it to all files in the
+current directory -- probably not what you want).
+
+.SH DISPLAYING MESSAGES
+
+We might also want to display the complete messages instead of the header
+information. This can be done using \fBmu view\fR command. Note that this
+command does not use the database; you simply provide it the path to a
+message.
+
+Therefore, if you want to display some message from a search query, you'll
+need its path. To get the path (think \fBl\fRocation) for our first example we
+can use:
+
+.nf
+ \fB$ mu find --fields="l" t:julius fruit\fR
+.fi
+
+And we'll get something like:
+.nf
+ /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2,
+.fi
+We can now display this message:
+
+.nf
+ \fB$ mu view /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2,\fR
+
+ From: John Milton <jm@example.com>
+ To: Julius Caesar <jc@example.com>
+ Subject: Fere libenter homines id quod volunt credunt
+ Date: 2008-07-31T21:57:25 EEST
+
+ OF Mans First Disobedience, and the Fruit
+ Of that Forbidden Tree, whose mortal taste
+ Brought Death into the World, and all our woe,
+ [...]
+.fi
+
+.SH FINDING CONTACTS
+
+While \fBmu find\fR searches for messages, there is also \fBmu cfind\fR to
+find \fIcontacts\fR, that is, names + addresses. Without any search
+expression, \fBmu cfind\fR lists all of your contacts.
+
+.nf
+ \fB$ mu cfind julius\fR
+.fi
+
+will find all contacts with 'julius' in either name or e-mail address. Note
+that \fBmu cfind\fR accepts a \fIregular expression\fR.
+
+\fBmu cfind\fR also supports a \fI--format=\fR-parameter, which sets the
+output to some specific format, so the results can be imported into another
+program. For example, to export your contact information to a \fBmutt\fR
+address book file, you can use something like:
+
+.nf
+ \fB$ mu cfind --format=mutt-alias > ~/mutt-aliases \fR
+.fi
+
+Then, you can use them in \fBmutt\fR if you add something like \fBsource
+~/mutt-aliases\fR to your \fImuttrc\fR.
+
+.SH AUTHOR
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+.BR mu (1),
+.BR mu-init (1),
+.BR mu-index (1),
+.BR mu-find (1),
+.BR mu-mfind (1),
+.BR mu-mkdir (1),
+.BR mu-view (1),
+.BR mu-extract (1)
--- /dev/null
+.TH MU EXTRACT 1 "July 2012" "User Manuals"
+
+.SH NAME
+
+\fBmu extract\fR is the \fBmu\fR command to display and save message parts
+(attachments), and open them with other tools.
+
+.SH SYNOPSIS
+
+.B mu extract [options] <file>
+
+.B mu extract [options] <file> <pattern>
+
+.SH DESCRIPTION
+
+\fBmu extract\fR is the \fBmu\fR sub-command for extracting MIME-parts (e.g.,
+attachments) from mail messages. The sub-command works on message files, and
+does not require the message to be indexed in the database.
+
+For attachments, the file name used when saving it is the name of the
+attachment in the message. If there is no such name, or when saving
+non-attachment MIME-parts, a name is derived from the message-id of the
+message.
+
+If you specify a pattern (a case-insensitive regular expression) as the second
+argument, all attachments with filenames matching that pattern will be
+extracted. The regular expressions are Perl-compatible (as per the
+PCRE-library).
+
+Without any options, \fBmu extract\fR simply outputs the list of leaf
+MIME-parts in the message. Only 'leaf' MIME-parts (including RFC822
+attachments) are considered, \fBmultipart/*\fR etc. are ignored.
+
+.SH OPTIONS
+
+.TP
+\fB\-a\fR, \fB\-\-save\-attachments\fR
+save all MIME-parts that look like attachments.
+
+.TP
+\fB\-\-save\-all\fR
+save all non-multipart MIME-parts.
+
+.TP
+\fB\-\-parts\fR=<parts>
+only consider the following numbered parts
+(comma-separated list). The numbers for the parts can be seen from running
+\fBmu extract\fR without any options but only the message file.
+
+.TP
+\fB\-\-target\-dir\fR=<dir>
+save the parts in the target directory rather than
+the current working directory.
+
+.TP
+\fB\-\-overwrite\fR
+overwrite existing files with the same name; by default overwriting is not
+allowed.
+
+.TP
+\fB\-\-play\fR Try to 'play' (open) the attachment with the default
+application for the particular file type. On MacOS, this uses the \fBopen\fR
+program, on other platforms it uses \fBxdg-open\fR. You can choose a different
+program by setting the \fBMU_PLAY_PROGRAM\fR environment variable.
+
+.SH EXAMPLES
+
+To display information about all the MIME-parts in a message file:
+.nf
+ $ mu extract msgfile
+.fi
+
+To extract MIME-part 3 and 4 from this message, overwriting existing files
+with the same name:
+.nf
+ $ mu extract --parts=3,4 --overwrite msgfile
+.fi
+
+To extract all files ending in '.jpg' (case-insensitive):
+.nf
+ $ mu extract msgfile '.*\.jpg'
+.fi
+
+To extract an mp3-file, and play it in the default mp3-playing application:
+.nf
+ $ mu extract --play msgfile 'whoopsididitagain.mp3'
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1)
--- /dev/null
+.TH MU FIELDS 1 "April 2022" "User Manuals"
+
+.SH NAME
+
+mu fields\- list all message fields
+
+.SH SYNOPSIS
+
+.B mu fields [options]
+
+.SH DESCRIPTION
+
+\fBmu fields\fR is the \fBmu\fR command for showing a table of message fields
+and their properties.
+
+.SH OPTIONS
+
+Inherits common options from
+.BR mu(1)
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1)
--- /dev/null
+.TH MU FIND 1 "29 April 2022" "User Manuals"
+
+.SH NAME
+
+mu find \- find e-mail messages in the \fBmu\fR database.
+
+.SH SYNOPSIS
+
+.B mu find [options] <search expression>
+
+.SH DESCRIPTION
+
+\fBmu find\fR is the \fBmu\fR command for searching e-mail message
+that were stored earlier using \fBmu index\fR(1).
+
+.SH SEARCHING MAIL
+
+\fBmu find\fR starts a search for messages in the database that match
+some search pattern. The search patterns are described in detail in
+.BR mu-query (7).
+.
+
+For example:
+
+.nf
+ $ mu find subject:snow and date:2009..
+.fi
+
+would find all messages in 2009 with 'snow' in the subject field, e.g:
+
+.nf
+ 2009-03-05 17:57:33 EET Lucia <lucia@example.com> running in the snow
+ 2009-03-05 18:38:24 EET Marius <marius@foobar.com> Re: running in the snow
+.fi
+
+Note, this the default, plain-text output, which is the default, so you don't
+have to use \fB--format=plain\fR. For other types of output (such as symlinks,
+XML or s-expressions), see the discussion in the \fBOPTIONS\fR-section below
+about \fB--format\fR.
+
+The search pattern is taken as a command-line parameter. If the search
+parameter consists of multiple parts (as in the example) they are
+treated as if there were a logical \fBand\fR between them.
+
+For details on the possible queries, see
+.BR mu-query (7).
+
+.SH OPTIONS
+
+Note, some of the important options are described in the \fBmu\fR(1) man-page
+and not here, as they apply to multiple mu-commands.
+
+The \fBfind\fR-command has various options that influence the way \fBmu\fR
+displays the results. If you don't specify anything, the defaults are
+\fI\-\-fields="d f s"\fR, \fI\-\-sortfield=date\fR and \fI\-\-reverse\fR.
+
+.TP
+\fB\-f\fR, \fB\-\-fields\fR=\fI<fields>\fR
+specifies a string that determines which fields are shown in the output. This
+string consists of a number of characters (such as 's' for subject or 'f' for
+from), which will replace with the actual field in the output. Fields that are
+not known will be output as-is, allowing for some simple formatting.
+
+For example:
+
+.nf
+ $ mu find subject:snow --fields "d f s"
+.fi
+
+would list the date, subject and sender of all messages with 'snow' in the
+their subject.
+
+The table of replacement characters is superset of the list mentions for
+search parameters, such as:
+.nf
+ t \fBt\fRo: recipient
+ d Sent \fBd\fRate of the message
+ f Message sender (\fBf\fRrom:)
+ g Message flags (fla\fBg\fRs)
+ l Full path to the message (\fBl\fRocation)
+ s Message \fBs\fRubject
+ i Message-\fBi\fRd
+ m \fBm\fRaildir
+.fi
+
+For the complete, up-to-date list, see:
+.BR mu-fields(1)
+
+The message flags are described in \fBmu-query\fR(7). As an example, a
+message which is 'seen', has an attachment and is signed would
+have 'asz' as its corresponding output string, while an encrypted new
+message would have 'nx'.
+
+.TP
+\fB\-s\fR, \fB\-\-sortfield\fR \fR=\fI<field>\fR and \fB\-z\fR,
+\fB\-\-reverse\fR specifies the field to sort the search results by, and the
+direction (i.e., 'reverse' means that the sort should be reverted - Z-A). Examples include:
+
+.nf
+ cc,c Cc (carbon-copy) recipient(s)
+ date,d Message sent date
+ from,f Message sender
+ maildir,m Maildir
+ msgid,i Message id
+ prio,p Nessage priority
+ subject,s Message subject
+ to,t To:-recipient(s)
+.fi
+
+For the complete list use can use the \fBmu fields\fR command; see:
+.BR mu-fields(1)
+
+Thus, for example, to sort messages by date, you could specify:
+
+.nf
+ $ mu find fahrrad --fields "d f s" --sortfield=date --reverse
+.fi
+
+Note, if you specify a sortfield, by default, messages are sorted in reverse
+(descending) order (e.g., from lowest to highest). This is usually a good
+choice, but for dates it may be more useful to sort in the opposite direction.
+
+.TP
+\fB\-n\fR, \fB\-\-maxnum=<number>\fR
+If > 0, display maximally that number of entries. If not specified, all matching entries are displayed.
+
+.TP
+\fB\-\-summary-len=<number>\fR
+If > 0, use that number of lines of the message to provide a summary.
+
+.TP
+\fB\-\-format\fR=\fIplain|links|xquery|xml|sexp\fR
+output results in the specified format.
+
+The default is \fBplain\fR, i.e normal output with one line per message.
+
+\fBlinks\fR outputs the results as a maildir with symbolic links to the found
+messages. This enables easy integration with mail-clients (see below for more
+information).
+
+\fBxml\fR formats the search results as XML.
+
+\fBsexp\fR formats the search results as an s-expression as used in Lisp
+programming environments.
+
+\fBxquery\fR shows the Xapian query corresponding to your search terms. This
+is meant for for debugging purposes.
+
+.TP
+\fB\-\-linksdir\fR \fR=\fI<dir>\fR and \fB\-c\fR, \fB\-\-clearlinks\fR
+output the results as a maildir with symbolic links to the found
+messages. This enables easy integration with mail-clients (see below
+for more information). \fBmu\fR will create the maildir if it does not
+exist yet.
+
+If you specify \fB\-\-clearlinks\fR, all existing symlinks will be
+cleared from the target directories; this allows for re-use of the
+same maildir. However, this option will delete any symlink it finds,
+so be careful.
+
+.nf
+ $ mu find grolsch --format=links --linksdir=~/Maildir/search --clearlinks
+.fi
+
+will store links to found messages in \fI~/Maildir/search\fR. If the directory
+does not exist yet, it will be created.
+
+Note: when \fBmu\fR creates a Maildir for these links, it automatically
+inserts a \fI.noindex\fR file, to exclude the directory from \fBmu
+index\fR.
+
+.TP
+\fB\-\-after=\fR\fI<timestamp>\fR only show messages whose message files were
+last modified (\fBmtime\fR) after \fI<timestamp>\fR. \fI<timestamp>\fR is a
+UNIX \fBtime_t\fR value, the number of seconds since 1970-01-01 (in UTC).
+
+From the command line, you can use the \fBdate\fR command to get this
+value. For example, only consider messages modified (or created) in the last 5
+minutes, you could specify
+.nf
+ --after=`date +%s --date='5 min ago'`
+.fi
+This is assuming the GNU \fBdate\fR command.
+
+
+.TP
+\fB\-\-exec\fR=\fI<command>\fR
+the \fB\-\-exec\fR command causes the \fIcommand\fR to be executed on each
+matched message; for example, to see the raw text of all messages
+matching 'milkshake', you could use:
+.nf
+ $ mu find milkshake --exec='less'
+.fi
+which is roughly equivalent to:
+.nf
+ $ mu find milkshake --fields="l" | xargs less
+.fi
+
+
+.TP
+\fB\-b\fR, \fB\-\-bookmark\fR=\fI<bookmark>\fR
+use a bookmarked search query. Using this option, a query from your bookmark
+file will be prepended to other search queries. See \fBmu-bookmarks\fR(1) for the
+details of the bookmarks file.
+
+
+.TP
+\fB\-\-skip\-dups\fR,\fB-u\fR whenever there are multiple messages with the
+same name, only show the first one. This is useful if you have copies of the
+same message, which is a common occurrence when using e.g. Gmail together with
+\fBofflineimap\fR.
+
+.TP
+\fB\-\-include\-related\fR,\fB-r\fR also include messages being referred to by
+the matched messages -- i.e.. include messages that are part of the same
+message thread as some matched messages. This is useful if you want
+Gmail-style 'conversations'. Note, finding these related messages make
+searches slower.
+
+.TP
+\fB\-t\fR, \fB\-\-threads\fR show messages in a 'threaded' format --
+that is, with indentation and arrows showing the conversation threads
+in the list of matching messages. When using this, sorting is
+chronological (by date), based on the newest message in a thread.
+
+Messages in the threaded list are indented based on the depth in the
+discussion, and are prefix with a kind of arrow with thread-related
+information about the message, as in the following table:
+
+.nf
+| | normal | orphan | duplicate |
+|-------------+--------+--------+-----------|
+| first child | `-> | `*> | `=> |
+| other | |-> | |*> | |=> |
+.fi
+
+Here, an 'orphan' is a message without a parent message (in the list of
+matches), and a duplicate is a message whose message-id was already seen
+before; not this may not really be the same message, if the message-id was
+copied.
+
+The algorithm used for determining the threads is based on Jamie Zawinksi's
+description:
+.BR http://www.jwz.org/doc/threading.html
+
+
+.SS Integrating mu find with mail clients
+
+.TP
+
+\fBmutt\fR
+
+For \fBmutt\fR you can use the following in your \fImuttrc\fR; pressing the F8
+key will start a search, and F9 will take you to the results.
+
+.nf
+# mutt macros for mu
+macro index <F8> "<shell-escape>mu find --clearlinks --format=links --linksdir=~/Maildir/search " \\
+ "mu find"
+macro index <F9> "<change-folder-readonly>~/Maildir/search" \\
+ "mu find results"
+.fi
+
+
+.TP
+
+\fBWanderlust\fR
+
+\fBSam B\fR suggested the following on the \fBmu\fR-mailing list. First add
+the following to your Wanderlust configuration file:
+
+.nf
+(require 'elmo-search)
+(elmo-search-register-engine
+ 'mu 'local-file
+ :prog "/usr/local/bin/mu" ;; or wherever you've installed it
+ :args '("find" pattern "--fields" "l") :charset 'utf-8)
+
+(setq elmo-search-default-engine 'mu)
+;; for when you type "g" in folder or summary.
+(setq wl-default-spec "[")
+.fi
+
+Now, you can search using the \fBg\fR key binding; you can also create
+permanent virtual folders when the messages matching some expression by adding
+something like the following to your \fIfolders\fR file.
+
+.nf
+VFolders {
+ [date:today..now]!mu "Today"
+
+ [size:1m..100m]!mu "Big"
+
+ [flag:unread]!mu "Unread"
+}
+.fi
+
+After restarting Wanderlust, the virtual folders should appear.
+
+.SH RETURN VALUE
+
+\fBmu find\fR returns 0 upon successful completion; if the search was
+performed, there needs to be a least one match. Anything else leads to a
+non-zero return value, for example:
+
+.nf
+| code | meaning |
+|------+--------------------------------|
+| 0 | ok |
+| 1 | general error |
+| 4 | no matches (for 'mu find') |
+.fi
+
+
+.SH ENCODING
+
+\fBmu find\fR output is encoded according the locale for \fI--format=plain\fR
+(the default), and UTF-8 for all other formats (\fIsexp\fR,
+\fIxml\fR).
+
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+If you have specific messages which are not matched correctly, please attach
+them (appropriately censored if needed).
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR mu-index (1),
+.BR mu-query (7)
+.BR mu-fields (1)
--- /dev/null
+.TH MU HELP 1 "July 2012" "User Manuals"
+
+.SH NAME
+
+\fBmu help\fR is a \fBmu\fR command that gives help information about mu
+commands.
+
+.SH SYNOPSIS
+
+.B mu help <command>
+
+.SH DESCRIPTION
+
+\fBmu help\fR provides help information about mu commands.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu-index (1),
+.BR mu-find (1),
+.BR mu-cfind (1),
+.BR mu-mkdir (1),
+.BR mu-view (1),
+.BR mu-extract (1),
+.BR mu-easy (1),
+.BR mu-bookmarks (5)
--- /dev/null
+.TH MU-INDEX 1 "June 2022" "User Manuals"
+
+.SH NAME
+
+mu index \- index e-mail messages stored in Maildirs
+
+.SH SYNOPSIS
+
+.B mu index [options]
+
+.SH DESCRIPTION
+
+\fBmu index\fR is the \fBmu\fR command for scanning the contents of Maildir
+directories and storing the results in a Xapian database. The data can then be
+queried using
+.BR mu-find (1)\.
+
+Before the first time you run \fBmu index\fR, you must run \fBmu init\fR to
+initialize the database.
+
+\fBindex\fR understands Maildirs as defined by Daniel Bernstein for
+\fBqmail\fR(7). In addition, it understands recursive Maildirs (Maildirs within
+Maildirs), Maildir++. It can also deal with VFAT-based Maildirs which use '!'
+or ';' as the separators instead of ':'.
+
+E-mail messages which are not stored in something resembling a maildir
+leaf-directory (\fIcur\fR and \fInew\fR) are ignored, as are the cache
+directories for \fInotmuch\fR and \fIgnus\fR, and any dot-directory.
+
+Starting with mu 1.5.x, symlinks are followed, and can be spread over multiple
+filesystems; however note that moving files around is much faster when multiple
+filesystems are not involved.
+
+If there is a file called \fI.noindex\fR in a directory, the contents of that
+directory and all of its subdirectories will be ignored. This can be useful to
+exclude certain directories from the indexing process, for example directories
+with spam-messages.
+
+If there is a file called \fI.noupdate\fR in a directory, the contents of that
+directory and all of its subdirectories will be ignored, unless we do a full
+rebuild (with \fBmu init\fR). This can be useful to speed up things you have
+some maildirs that never change. Note that you can still search for these
+messages, this only affects updating the database. \fI.noupdate\fR is ignored
+when you start indexing with an empty database (such as directly after \fImu
+init\fR.
+
+There also the \fB--lazy-check\fR which can greatly speed up indexing; see below
+for details.
+
+The first run of \fBmu index\fR may take a few minutes if you have a lot of mail
+(tens of thousands of messages). Fortunately, such a full scan needs to be done
+only once; after that it suffices to index the changes, which goes much faster.
+See the 'Note on performance (i,ii,iii)' below for more information.
+
+The optional 'phase two' of the indexing-process is the removal of messages from
+the database for which there is no longer a corresponding file in the Maildir.
+If you do not want this, you can use \fB\-n\fR, \fB\-\-nocleanup\fR.
+
+When \fBmu index\fR catches one of the signals \fBSIGINT\fR, \fBSIGHUP\fR or
+\fBSIGTERM\fR (e.g., when you press Ctrl-C during the indexing process), it
+tries to shutdown gracefully; it tries to save and commit data, and close the
+database etc. If it receives another signal (e.g., when pressing Ctrl-C once
+more), \fBmu index\fR will terminate immediately.
+
+.SH OPTIONS
+
+Some of the general options are described in the \fBmu(1)\fR man-page and not
+here, as they apply to multiple mu commands.
+
+.TP
+\fB\-\-lazy-check\fR
+in lazy-check mode, \fBmu\fR does not consider messages for which the
+time-stamp (ctime) of the directory they reside in has not changed
+since the previous indexing run. This is much faster than the non-lazy
+check, but won't update messages that have change (rather than having
+been added or removed), since merely editing a message does not update
+the directory time-stamp. Of course, you can run \fBmu-index\fR
+occasionally without \fB\-\-lazy-check\fR, to pick up such messages.
+
+.TP
+\fB\-\-nocleanup\fR
+disables the database cleanup that \fBmu\fR does by default after indexing.
+
+.SS A note on performance (i)
+As a non-scientific benchmark, a simple test on the author's machine (a
+Thinkpad X61s laptop using Linux 2.6.35 and an ext3 file system) with no
+existing database, and a maildir with 27273 messages:
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 66,65s user 6,05s system 27% cpu 4:24,20 total
+.fi
+(about 103 messages per second)
+
+A second run, which is the more typical use case when there is a database
+already, goes much faster:
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 0,48s user 0,76s system 10% cpu 11,796 total
+.fi
+(more than 56818 messages per second)
+
+Note that each test flushes the caches first; a more common use case might
+be to run \fBmu index\fR when new mail has arrived; the cache may stay
+quite 'warm' in that case:
+
+.nf
+ $ time mu index --quiet
+ 0,33s user 0,40s system 80% cpu 0,905 total
+.fi
+which is more than 30000 messages per second.
+
+
+.SS A note on performance (ii)
+As per June 2012, we did the same non-scientific benchmark, this time with an
+Intel i5-2500 CPU @ 3.30GHz, an ext4 file system and a maildir with 22589
+messages. We start without an existing database.
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 27,79s user 2,17s system 48% cpu 1:01,47 total
+.fi
+(about 813 messages per second)
+
+A second run, which is the more typical use case when there is a database
+already, goes much faster:
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 0,13s user 0,30s system 19% cpu 2,162 total
+.fi
+(more than 173000 messages per second)
+
+
+.SS A note on performance (iii)
+As per July 2016, we did the same non-scientific benchmark, again with
+the Intel i5-2500 CPU @ 3.30GHz, an ext4 file system. This time, the
+maildir contains 72525 messages.
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 40,34s user 2,56s system 64% cpu 1:06,17 total
+.fi
+(about 1099 messages per second).
+
+.SS A note on performance (iv)
+A few years later and its June 2022. There's a lot more happening during indexing, but indexing became multi-threaded and machines are faster; e.g. this
+is with an AMD Ryzen Threadripper 1950X (32) @ 3.399GHz.
+
+The instructions are a little different since we have a proper repeatable
+benchmark now. After building,
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+% THREAD_NUM=4 build/lib/tests/bench-indexer -m perf
+# random seed: R02Sf5c50e4851ec51adaf301e0e054bd52b
+1..1
+# Start of bench tests
+# Start of indexer tests
+indexed 5000 messages in 20 maildirs in 3763ms; 752 μs/message; 1328 messages/s (4 thread(s))
+ok 1 /bench/indexer/4-cores
+# End of indexer tests
+# End of bench tests
+.fi
+
+Things are again a little faster, even though the index does a lot more now
+(text-normalizatian, and pre-generating message-sexps). A faster machine helps,
+too!
+
+.SH RETURN VALUE
+
+\fBmu index\fR return 0 upon successful completion; any other number signals an
+error.
+
+.SH BUGS
+
+Please report bugs if you find any:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR maildir (5),
+.BR mu (1),
+.BR mu-init (1),
+.BR mu-find (1),
+.BR mu-cfind (1)
--- /dev/null
+.TH MU-INFO 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+mu info \- show information about the mu database
+
+.SH SYNOPSIS
+
+.B mu info [options]
+
+.SH DESCRIPTION
+
+\fBmu info\fR is the \fBmu\fR command for getting information about the mu
+database. Note that while running (e.g. \fBmu4e\fR), some of the information
+may be slightly delayed due to database caching.
+
+.SH OPTIONS
+
+Note, some of the general options are described in the \fBmu(1)\fR man-page and
+not here, as they apply to multiple mu commands.
+
+.TP
+\fB\-\-muhome\fR
+use an alternative directory to store and read the database, write the logs,
+etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux
+this defaults to \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of
+\fBmu\fR defaulted to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR.
+
+.SH RETURN VALUE
+
+\fBmu init\fR returns 0 upon successful completion, or a non-zero exit code if
+there was some error.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR maildir (5),
+.BR mu (1),
+.BR mu-index (1)
--- /dev/null
+.TH MU-INIT 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+mu init \- initialize the mu message database
+
+.SH SYNOPSIS
+
+.B mu init [options]
+
+.SH DESCRIPTION
+
+\fBmu init\fR is the subcommand for setting up the mu message
+database. After \fBmu init\fR has completed, you can run \fBmu
+index\fR
+
+.SH OPTIONS
+
+Note, some of the general options are described in the \fBmu(1)\fR
+man-page and not here, as they apply to multiple mu commands.
+
+.TP
+\fB\-\-muhome\fR
+use an alternative directory to store and read the database, write the logs,
+etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux
+this defaults to \fI~/.cache/mu\fR, \fI~/.config/mu\fR).
+
+Earlier versions of \fBmu\fR defaulted to \fI~/.mu\fR, which now requires
+\fI\-\-muhome=~/.mu\fR.
+
+Alternatively, use can use the \fBMUHOME\fR environment variable (the command-line takes precedence).
+
+.TP
+\fB\-m\fR, \fB\-\-maildir\fR=\fI<maildir>\fR
+starts searching at \fI<maildir>\fR. By default, \fBmu\fR uses whatever the
+\fBMAILDIR\fR environment variable is set to; if it is not set, it tries
+\fI~/Maildir\fR if it already exists.
+
+.TP
+\fB\-\-my-address\fR=\fI<my-email-address>\fR
+specifies that some e-mail addresses are 'my-address' (\fB\-\-my-address\fR can
+be used multiple times). This is used by \fBmu cfind\fR -- any e-mail address
+found in the address fields of a message which also has \fI<my-email-address>\fR
+in one of its address fields is considered a \fIpersonal\fR e-mail address. This
+allows you, for example, to filter out (\fBmu cfind --personal\fR) addresses
+which were merely seen in mailing list messages.
+
+\fI<my-email-address>\fR can be either a plain e-mail address (such as
+\fBfoo@example.com\fR), or a regular-expression (of the 'Basic POSIX' flavor),
+wrapped in \fB/\fR (such as \fB/foo-.*@example\\.com/\fR). Depending on your
+shell program, the argument may need to b quoted.
+
+.SH ENVIRONMENT
+
+\fBmu init\fR uses \fBMAILDIR\fR to find the user's Maildir if it has not been
+specified explicitly with \fB\-\-maildir\fR=\fI<maildir>\fR. If \fBMAILDIR\fR is
+not set, \fBmu init\fR uses \fI~/Maildir\fR.
+
+\fBMUHOME\fR can be used as an alternative to \fB\-\-muhome\fR.
+
+.SH RETURN VALUE
+
+\fBmu init\fR returns 0 upon successful completion, or a non-zero exit code if
+there was some error.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR maildir (5),
+.BR mu (1),
+.BR mu-index (1)
--- /dev/null
+.TH MU MKDIR 1 "July 2012" "User Manuals"
+
+.SH NAME
+
+mu mkdir\- create a new Maildir
+
+.SH SYNOPSIS
+
+.B mu mkdir [options] <dir> [<dirs>]
+
+.SH DESCRIPTION
+
+\fBmu mkdir\fR is the \fBmu\fR command for creating Maildirs. It does
+\fBnot\fR use the mu database. With the \fBmkdir\fR command, you can create
+new Maildirs with permissions 0755. For example,
+
+.nf
+ mu mkdir tom dick harry
+.fi
+
+creates three maildirs, \fItom\fR, \fIdick\fR and \fIharry\fR.
+
+If creation fails for any reason, \fBno\fR attempt is made to remove any parts
+that were created. This is for safety reasons.
+
+.SH OPTIONS
+
+.TP
+\fB\-\-mode\fR=<mode>
+set the file access mode for the new maildir(s) as in \fBchmod(1)\fR.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR maildir (5),
+.BR mu (1),
+.BR chmod (1)
--- /dev/null
+.TH MU QUERY 7 "22 April 2022" "User Manuals"
+
+.SH NAME
+
+mu query language \- a language for finding messages in \fBmu\fR databases.
+
+.SH DESCRIPTION
+
+The mu query language is a language used by \fBmu find\fR and \fBmu4e\fR to find
+messages in \fBmu\fR's Xapian databases. The language is quite similar to
+Xapian's default query-parser, but is an independent implementation that is
+customized for the mu/mu4e use-case.
+
+In this article, we give a structured but informal overview of the query
+language and provide examples.
+
+As a companion to this, we recommend the \fBmu fields\fR and \fBmu flags\fR
+commands to get an up-to-date list of the available fields and flags.
+
+\fBNOTE:\fR if you use queries on the command-line (say, for \fBmu find\fR), you
+need to quote any characters that would otherwise be interpreted by the shell,
+such as \fB""\fR, \fB(\fR and \fB)\fR and whitespace.
+
+.de EX1
+.nf
+.RS
+..
+
+.de EX2
+.RE
+.fi
+..
+
+.SH TERMS
+
+The basic building blocks of a query are \fBterms\fR; these are just
+normal words like 'banana' or 'hello', or words prefixed with a
+field-name which make them apply to just that field. See
+\fBmu find\fR
+for all the available fields.
+
+Some example queries:
+.EX1
+vacation
+subject:capybara
+maildir:/inbox
+.EX2
+
+Terms without an explicit field-prefix, (like 'vacation' above) are
+interpreted like:
+.EX1
+to:vacation or subject:vacation or body:vacation or ...
+.EX2
+
+The language is case-insensitive for terms and attempts to 'flatten'
+any diacritics, so \fIangtrom\fR matches \fIÅngström\fR.
+
+.PP
+If terms contain whitespace, they need to be quoted:
+.EX1
+subject:"hi there"
+.EX2
+This is a so-called \fIphrase query\fR, which means that we match
+against subjects that contain the literal phrase "hi there".
+
+Remember that you need to escape those quotes when using this from the
+command-line:
+.EX1
+mu find subject:\\"hi there\\"
+.EX2
+
+.SH LOGICAL OPERATORS
+
+We can combine terms with logical operators -- binary ones: \fBand\fR,
+\fBor\fR, \fBxor\fR and the unary \fBnot\fR, with the conventional
+rules for precedence and association, and are case-insensitive.
+
+.PP
+You can also group things with \fB(\fR and \fB)\fR, so you can do
+things like:
+.EX1
+(subject:beethoven or subject:bach) and not body:elvis
+.EX2
+
+If you do not explicitly specify an operator between terms, \fBand\fR
+is implied, so the queries
+.EX1
+subject:chip subject:dale
+.EX2
+.EX1
+subject:chip AND subject:dale
+.EX2
+are equivalent. For readability, we recommend the second version.
+
+Note that a \fIpure not\fR - e.g. searching for \fBnot apples\fR is
+quite a 'heavy' query.
+
+.SH REGULAR EXPRESSIONS AND WILDCARDS
+
+The language supports matching regular expressions that follow
+ECMAScript; for details, see
+
+.BR http://www.cplusplus.com/reference/regex/ECMAScript/
+
+Regular expressions must be enclosed in \fB//\fR. Some examples:
+.EX1
+subject:/h.llo/ # match hallo, hello, ...
+subject:/
+.EX2
+
+Note the difference between 'maildir:/foo' and 'maildir:/foo/'; the
+former matches messages in the '/foo' maildir, while the latter
+matches all messages in all maildirs that match 'foo', such
+as '/foo', '/bar/cuux/foo', '/fooishbar' etc.
+
+Wildcards are an older mechanism for matching where a term with a
+rightmost \fB*\fR (and \fIonly\fR in that position) matches any term
+that starts with the part before the \fB*\fR; they are supported for
+backward compatibility and \fBmu\fR translates them to regular
+expressions internally:
+.EX1
+foo*
+.EX2
+is equivalent to
+.EX1
+/foo.*/
+.EX2
+
+As a note of caution, certain wild-cards and regular expression can
+take quite a bit longer than 'normal' queries.
+
+.SH FIELDS
+
+We already saw a number of search fields, such as \fBsubject:\fR and
+\fBbody:\fR. For the full table, see \fBmu fields\fR.
+.EX1
+ bcc,h Bcc (blind-carbon-copy) recipient(s)
+ body,b Message body
+ cc,c Cc (carbon-copy) recipient(s)
+ changed,k Last change to message file (range)
+ date,d Send date (range)
+ embed,e Search inside embedded text parts
+ file,j Attachment filename
+ flag,g Message Flags
+ from,f Message sender
+ list,v Mailing list (e.g. the List-Id value)
+ maildir,m Maildir
+ mime,y MIME-type of one or more message parts
+ msgid,i Message-ID
+ prio,p Message priority (\fIlow\fR, \fInormal\fR or \fIhigh\fR)
+ size,z Message size range
+ subject,s Message subject
+ tag,x Tags for the message
+ thread,w Thread a message belongs to
+ to,t To: recipient(s)
+
+The \fBmu fields\fR command is recommended to get the latest version.
+.EX2
+The shortcut character can be used instead of the full name:
+.EX1
+f:foo@bar
+.EX2
+is the same as
+.EX1
+from:foo@bar
+.EX2
+For queries that are not one-off, we would recommend the longer name
+for readability.
+
+There are also the special fields \fBcontact:\fR, which matches all
+contact-fields (\fIfrom\fR, \fIto\fR, \fIcc\fR and \fIbcc\fR), and
+\fBrecip\fR, which matches all recipient-fields (\fIto\fR, \fIcc\fR
+and \fIbcc\fR). Hence, for instance,
+.EX1
+contact:fnorb@example.com
+.EX2
+is equivalent to
+.EX1
+(from:fnorb@example.com or to:fnorb@example.com or
+ cc:from:fnorb@example.com or bcc:fnorb@example.com)
+.EX2
+
+.SH DATE RANGES
+
+The \fBdate:\fR field takes a date-range, expressed as the lower and
+upper bound, separated by \fB..\fR. Either lower or upper (but not
+both) can be omitted to create an open range.
+
+Dates are expressed in local time and using ISO-8601 format
+(YYYY-MM-DD HH:MM:SS); you can leave out the right part, and \fBmu\fR
+adds the rest, depending on whether this is the beginning or end of
+the range (e.g., as a lower bound, '2015' would be interpreted as the
+start of that year; as an upper bound as the end of the year).
+
+You can use '/' , '.', '-' and 'T' to make dates more human readable.
+
+Some examples:
+.EX1
+date:20170505..20170602
+date:2017-05-05..2017-06-02
+date:..2017-10-01T12:00
+date:2015-06-01..
+date:2016..2016
+.EX2
+
+You can also use the special 'dates' \fBnow\fR and \fBtoday\fR:
+.EX1
+date:20170505..now
+date:today..
+.EX2
+
+Finally, you can use relative 'ago' times which express some time
+before now and consist of a number followed by a unit, with units
+\fBs\fR for seconds, \fBM\fR for minutes, \fBh\fR for hours, \fBd\fR
+for days, \fBw\fR for week, \fBm\fR for months and \fBy\fR for years.
+Some examples:
+
+.EX1
+date:3m..
+date:2017.01.01..5w
+.EX2
+
+.SH SIZE RANGES
+
+The \fBsize\fR or \fBz\fR field allows you to match \fIsize ranges\fR
+-- that is, match messages that have a byte-size within a certain
+range. Units (b (for bytes), K (for 1000 bytes) and M (for 1000 * 1000
+bytes) are supported). Some examples:
+
+.EX1
+size:10k..2m
+size:10m..
+.EX2
+
+.SH FLAG FIELDS
+
+The \fBflag\fR/\fBg\fR field allows you to match message flags. The
+following fields are available:
+.EX1
+ a,attach Message with attachment
+ d,draft Draft Message
+ f,flagged Flagged
+ l,list Mailing-list message
+ n,new New message (in new/ Maildir)
+ p,passed Passed ('Handled')
+ r,replied Replied
+ s,seen Seen
+ t,trashed Marked for deletion
+ u,unread new OR NOT seen
+ x,encrypted Encrypted message
+ z,signed Signed message
+.EX2
+
+Some examples:
+.EX1
+flag:attach
+flag:replied
+g:x
+.EX2
+
+Encrypted messages may be signed as well, but this is only visible
+after decrypting and thus, invisible to \fBmu\fR.
+
+.SH PRIORITY FIELD
+
+The message priority field (\fBprio:\fR) has three possible values:
+\fBlow\fR, \fBnormal\fR or \fBhigh\fR. For instance, to match
+high-priority messages:
+.EX1
+ prio:high
+.EX2
+
+.SH MAILDIR
+
+The Maildir field describes the directory path starting \fBafter\fR
+the Maildir-base path, and before the \fI/cur/\fR or \fI/new/\fR part.
+So for example, if there's a message with the file name
+\fI~/Maildir/lists/running/cur/1234.213:2,\fR, you could find it (and
+all the other messages in the same maildir) with:
+.EX1
+maildir:/lists/running
+.EX2
+
+Note the starting '/'. If you want to match mails in the 'root'
+maildir, you can do with a single '/':
+.EX1
+maildir:/
+.EX2
+
+If you have maildirs (or any fields) that include spaces, you need to
+quote them, ie.
+.EX1
+maildir:"/Sent Items"
+.EX2
+
+Note that from the command-line, such queries must be quoted:
+.EX1
+mu find 'maildir:"/Sent Items"'
+.EX2
+
+.SH MORE EXAMPLES
+
+Here are some simple examples of \fBmu\fR queries; you can make many
+more complicated queries using various logical operators, parentheses
+and so on, but in the author's experience, it's usually faster to find
+a message with a simple query just searching for some words.
+
+Find all messages with both 'bee' and 'bird' (in any field)
+.EX1
+bee AND bird
+.EX2
+
+Find all messages with either Frodo or Sam:
+.EX1
+Frodo OR Sam
+.EX2
+
+Find all messages with the 'wombat' as subject, and 'capybara' anywhere:
+.EX1
+subject:wombat and capybara
+.EX2
+
+Find all messages in the 'Archive' folder from Fred:
+.EX1
+from:fred and maildir:/Archive
+.EX2
+
+Find all unread messages with attachments:
+.EX1
+flag:attach and flag:unread
+.EX2
+
+
+Find all messages with PDF-attachments:
+.EX1
+mime:application/pdf
+.EX2
+
+Find all messages with attached images:
+.EX1
+mime:image/*
+.EX2
+
+.SH CAVEATS
+
+With current Xapian versions, the apostroph character is considered part of a
+word. Thus, you cannot find \fID'Artagnan\fR by searching for \fIArtagnan\fR.
+So, include the apostroph in search or use a regexp search.
+
+Matching on spaces has changed compared to the old query-parser; this applies
+e.g. to Maildirs that have spaces in their name, such as \fISent Items\fR. See
+\fBMAILDIR\fR above.
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu-find (1)
+.BR mu-fields (1)
--- /dev/null
+.TH MU REMOVE 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+\fBmu remove\fR is the \fBmu\fR command to remove messages from the database.
+
+.SH SYNOPSIS
+
+.B mu remove [options] <file> [<files>]
+
+.SH DESCRIPTION
+
+\fBmu remove\fR removes specific messages from the database, each of them
+specified by their filename. The files do not have to exist in the file
+system.
+
+.SH OPTIONS
+
+\fBmu remove\fR does not have its own options, but the general options for
+determining the location of the database (\fI--muhome\fR) are available. See
+\fBmu-index(1)\fR for more information.
+
+.SH RETURN VALUE
+
+\fBmu remove\fR returns 0 upon success; in general, the following error codes are
+returned:
+
+.nf
+| code | meaning |
+|------+-----------------------------------|
+| 0 | ok |
+| 1 | general error |
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR mu-index (1),
+.BR mu-add (1)
--- /dev/null
+.TH MU SCRIPT 1 "October 2021" "User Manuals"
+
+.SH NAME
+
+mu script\- show the available mu scripts, and/or run them.
+
+.SH SYNOPSIS
+
+.B mu script [options] [<pattern>]
+
+.B mu <script-name> [<script-options>]
+
+.SH DESCRIPTION
+
+\fBmu script\fR is the \fBmu\fR command to list available \fBmu\fR scripts.
+The scripts are to be implemented in the Guile programming language, and
+therefore only work if your \fBmu\fR is built with support for Guile. In
+addition, many scripts require you to have \fBgnuplot\fR installed.
+
+Without any parameters, \fBmu script\fR lists the available scripts. If you
+provide a pattern (a regular expression), only the scripts whose name or
+one-line description match this pattern are listed. See the examples below.
+
+\fBmu\fR ships with a number of scripts.
+
+.SH OPTIONS
+
+.TP
+\fB\-\-verbose\fR,\fB\-v\fR
+when listing the available scripts, show the long descriptions.
+
+\fB\-\-\fR
+all options on the right side of the \fB\-\-\fR are passed to the script.
+
+.SH EXAMPLES
+
+List all available scripts (one-line descriptions):
+.nf
+ $ mu script
+.fi
+
+List all available scripts matching \fImonth\fR (long descriptions):
+.nf
+ $ mu script -v month
+.fi
+
+Run the \fImsgs-per-month\fR script for messages matching 'hello', and pass it
+the \fI--textonly\fR parameter:
+.nf
+ $ mu msgs-per-month --query=hello --textonly
+.fi
+
+.SH RETURN VALUE
+
+\fBmu script\fR returns 0 when all went well, and returns some non-zero error
+code when this is not the case.
+
+.SH FILES
+
+You can make your own Scheme scripts accessible through \fBmu script\fR by
+putting them in either \fI<XDG_DATA_HOME>/mu/scripts\fR (e.g., \fI~/.local/share/mu/scripts\fR) or, if \fImuhome\fR is specified, in
+
+It is a good idea to document the scripts by using some
+special comments in the source code:
+.nf
+;; INFO: this is my script -- one-line description
+;; INFO: (longer description)
+;; INFO: --option1=<foo> (describe option1)
+;; INFO: etc.
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR guile (1)
--- /dev/null
+.TH MU-SERVER 1 "January 2020" "User Manuals"
+
+.SH NAME
+
+mu server \- the mu backend for the mu4e e-mail client
+
+.SH SYNOPSIS
+
+.B mu server [options]
+
+.SH DESCRIPTION
+
+\fBmu server\fR starts a simple shell in which one can query and manipulate the
+mu database. The output uses s-expressions. \fBmu server\fR is not meant for use
+by humans, except for debugging purposes. Instead, it is designed specifically
+for the \fBmu4e\fR e-mail client.
+
+In this man-page, we document the commands \fBmu server\fR accepts, as well as
+their responses. In general, the commands sent to the server are s-expressions
+of the form:
+
+.nf
+ (<command-name> :param1 value1 :param2 value2)
+.fi
+
+For example, to view a certain message, the command would be:
+
+.nf
+ (view :docid 12345)
+.fi
+
+Parameters can be sent in any order; they must be of the correct type though.
+See \fBlib/utils/mu-sexp-parser.hh\fR and \fBlib/utils/mu-sexp-parser.cc\fR in
+source-tree for the details.
+
+
+.SH OUTPUT FORMAT
+
+\fBmu server\fR accepts a number of commands, and delivers its results in
+the form:
+
+.nf
+ \\376<length>\\377<s-expr>
+.fi
+
+\\376 (one byte 0xfe), followed by the length of the s-expression expressed as
+an hexadecimal number, followed by another \\377 (one byte 0xff), followed by
+the actual s-expression.
+
+By prefixing the expression with its length, it can be processed more
+efficiently. The \\376 and \\377 were chosen since they never occur in valid
+UTF-8 (in which the s-expressions are encoded).
+
+.sh COMMANDS
+
+
+.SH AUTHOR
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+.BR mu (1)
--- /dev/null
+.TH MU VERIFY 1 "April 2022" "User Manuals"
+
+.SH NAME
+
+mu verify\- verify message signatures and display information about them
+
+.SH SYNOPSIS
+
+.B mu verify [options] <msgfile>
+
+.SH DESCRIPTION
+
+\fBmu verify\fR is the \fBmu\fR command for verifying message signatures (such
+as PGP/GPG signatures) and displaying information about them. The sub-command
+works on message files, and does not require the message to be indexed in the
+database.
+
+.SH OPTIONS
+
+.TP
+\fB\-r\fR, \fB\-\-auto\-retrieve\fR
+attempt to find keys online (see the \fBauto-key-retrieve\fR option in the
+\fBgnupg(1)\fR documentation).
+
+.SH EXAMPLES
+
+To display aggregated (one-line) information about the verification status in a
+message:
+.nf
+ $ mu verify msgfile
+.fi
+
+To display information about all the signatures:
+.nf
+ $ mu verify --verbose msgfile
+.fi
+
+If you only want to use the exit code, you can use:
+.nf
+ $ mu verify --quiet msgfile
+.fi
+which does not give any output unless there is an error.
+
+.SH RETURN VALUE
+
+\fBmu verify\fR returns 0 when all signatures could be verified to be good, and
+returns some non-zero error code when this is not the case.
+
+When there are no signatures, returns 0 as well.
+
+.nf
+| code | meaning |
+|------+--------------------------------|
+| 0 | ok |
+| 1 | some non-verified signature(s) |
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1)
--- /dev/null
+.TH MU VIEW 1 "April 2022" "User Manuals"
+
+.SH NAME
+
+mu view\- display an e-mail message file
+
+.SH SYNOPSIS
+
+.B mu view [options] <file> [<files>]
+
+.SH DESCRIPTION
+
+\fBmu view\fR is the \fBmu\fR command for displaying e-mail message files. It
+works on message files and does \fInot\fR require the message to be indexed in
+the database.
+
+The command shows some common headers (From:, To:, Cc:, Bcc:, Subject: and
+Date:), the list of attachments and the plain-text body of the message (if
+any).
+
+.SH OPTIONS
+
+.TP
+\fB\-\-summary-len\fR=\fI<number>\fR
+instead of displaying the full message, output a summary based upon the first
+\fI<number>\fR lines of the message.
+
+.TP
+\fB\-\-terminate\fR
+terminate messages with \\f (\fIform-feed\fR) characters when displaying
+them. This is useful when you want to further process them.
+
+.TP
+\fB\-\-decrypt\fR
+attempt to decrypt encrypted message bodies. This is only possible if \fBmu\fR
+was built with crypto-support.
+
+.TP
+\fB\-\-auto-retrieve\fR
+attempt to retrieve crypto-keys automatically from the network, when needed.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1)
--- /dev/null
+.TH MU 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+mu \- a set of tools to deal with Maildirs and message files, in particular to
+index and search e-mail messages.
+
+.SH SYNOPSIS
+
+In alphabetical order:
+
+.B mu [options]
+general mu command.
+
+.B mu add
+add specific messages to the database. See
+.BR mu-add(1)
+
+.B mu cfind [options] [<regexp>]
+find contacts. See
+.BR mu-cfind(1)
+
+.B mu extract [options] <file> [<parts>] [<regexp>]
+extract attachments and other MIME-parts. See
+.BR mu-extract(1)
+
+.B mu find [options] <search expression>
+find messages. See
+.BR mu-find(1)
+
+.B mu help [command]
+get help for some command. See
+.BR mu-help(1)
+
+.B mu index [options]
+(re)index the messages in a Maildir. See
+.BR mu-index(1)
+
+.B mu info [options]
+show information about the mu database
+.BR mu-info(1)
+
+.B mu init [options]
+initialize the mu database
+.BR mu-init(1)
+
+.B mu mkdir [options] <dir> [<dirs>]
+create a new Maildir. See
+.BR mu-mkdir(1)
+
+.B mu remove [options]
+remove specific messages from the database. See
+.BR mu-remove(1)
+
+.B mu script [options]
+run a mu (Guile) script. See
+.BR mu-script(1)
+
+.B mu server [options]
+start a server process (for \fBmu4e\fR-internal use). See
+.BR mu-server(1)
+
+.B mu view <file> [<files>]
+view a specific message. See
+.BR mu-view(1)
+
+.SH DESCRIPTION
+
+\fBmu\fR is a set of tools for dealing with Maildirs and the e-mail messages
+in them.
+
+\fBmu\fR's main purpose is to enable searching of e-mail messages. It
+does so by periodically scanning a Maildir directory tree and
+analyzing the e-mail messages found (this is called 'indexing'). The
+results of this analysis are stored in a database, which can then be
+queried.
+
+In addition to indexing and searching, \fBmu\fR also offers
+functionality for viewing messages, extracting attachments and
+creating maildirs, and searching and exporting contact information.
+
+\fBmu\fR can be used from the command line or can be integrated with various
+e-mail clients.
+
+This manpage gives a general overview of the available commands
+(\fBindex\fR, \fBfind\fR, etc.); each \fBmu\fR command has its own
+man-page as well.
+
+.SH COLORS
+
+Some \fBmu\fR sub-commands support colorized output, and do so by
+default. If you don't want colors, you can use \fI--nocolor\fR.
+
+Currently, \fBmu find\fR, \fBmu view\fR, \fBmu cfind\fR and \fBmu extract\fR
+support colors.
+
+.SH ENCODING
+
+\fBmu\fR's output is in the current locale, with the exceptions of the output
+specifically meant for output to UTF8-encoded files. In practice, this means
+that the output of commands \fBindex\fR, \fBview\fR,
+\fBextract\fR is always encoded according to the current locale.
+
+The same is true for \fBfind\fR and \fBcfind\fR, with some exceptions, where
+the output is always UTF-8, regardless of the locale.
+
+For \fBcfind\fR the exception is \fI--format=bbdb\fR. This is hard-coded to
+UTF-8, and as such specified in the output-file, so emacs/bbdb can handle it
+correctly without guessing.
+
+For \fBfind\fR the output is encoded according the locale for
+\fI--format=plain\fR (the default), and UTF-8 for all other formats
+(\fIjson\fR, \fIsexp\fR, \fIxml\fR).
+
+.SH DATABASE AND FILE
+
+Commands \fBmu index\fR and \fBfind\fR and \fBcfind\fR work with the database,
+while the other ones work on individual mail files. Hence, running \fBview\fR,
+\fBmkdir\fR and \fBextract\fR does not require the mu database.
+
+The various commands are discussed in more detail in their own separate
+man-pages; here the general options are discussed.
+
+.SH OPTIONS
+
+\fBmu\fR offers several general options that apply to all commands,
+including \fBmu\fR without any command.
+
+.TP
+\fB\-\-muhome\fR
+use an alternative directory to store and read the database, write the logs,
+etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux
+by default \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of \fBmu\fR defaulted
+to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR.
+
+.TP
+\fB\-d\fR, \fB\-\-debug\fR
+makes \fBmu\fR generate extra debug information,
+useful for debugging the program itself. By default, debug information goes to
+the log file, \fI~/.cache/mu/mu.log\fR. It can safely be deleted when \fBmu\fR is
+not running. When running with \fB--debug\fR option, the log file can grow
+rather quickly. See the note on logging below.
+
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+causes \fBmu\fR not to output informational
+messages and progress information to standard output, but only to the log
+file. Error messages will still be sent to standard error. Note that \fBmu
+index\fR is \fBmuch\fR faster with \fB\-\-quiet\fR, so it is recommended you
+use this option when using \fBmu\fR from scripts etc.
+
+.TP
+\fB\-\-log-stderr\fR
+causes \fBmu\fR to \fBnot\fR output log messages to standard error, in
+addition to sending them to the log file.
+
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+prints \fBmu\fR version and copyright information.
+
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+lists the various command line options.
+
+.SH ENVIRONMENT
+
+\fBMUHOME\fR can be used as an alternative to \fB\-\-muhome\fR. The latter has precedence.
+
+\fBNO_COLOR\fR can be used as an alternative to \fB\-\-nocolor\fR.
+
+.SH ERROR CODES
+
+The various mu subcommands typically exit with 0 (zero) upon success, and
+non-zero when some error occurred.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+.BR mu-index (1), mu-find (1), mu-cfind (1), mu-mkdir (1), mu-view (1),
+.BR mu-extract (1), mu-easy (1), mu-bookmarks (5), mu-query (7)
+.BR https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
--- /dev/null
+## 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 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.
+
+################################################################################
+# project setup
+#
+project('mu', ['c', 'cpp'],
+ version: '1.8.14',
+ meson_version: '>= 0.52.0', # debian 10
+ license: 'GPL-3.0-or-later',
+ default_options : [
+ 'buildtype=debugoptimized',
+ 'warning_level=3',
+ 'c_std=c11',
+ 'cpp_std=c++17'
+ ]
+ )
+
+# installation paths
+prefixdir = get_option('prefix')
+bindir = prefixdir / get_option('bindir')
+datadir = prefixdir / get_option('datadir')
+mandir = prefixdir / get_option('mandir')
+infodir = prefixdir / get_option('infodir')
+
+# allow for configuring lispdir, as with autotools.
+# default to <p
+if get_option('lispdir') == ''
+ mu4e_lispdir= datadir / join_paths('emacs', 'site-lisp', 'mu4e')
+else
+ mu4e_lispdir= get_option('lispdir') / 'mu4e'
+endif
+
+################################################################################
+# compilers / flags
+#
+extra_flags = [
+ '-Wno-unused-parameter',
+ '-Wno-cast-function-type',
+ '-Wformat-security',
+ '-Wformat=2',
+ '-Wstack-protector',
+ '-Wno-switch-enum',
+ '-Wno-keyword-macro',
+ '-Wno-#warnings']
+
+if get_option('buildtype') == 'debug'
+ extra_flags += [
+ '-ggdb',
+ '-fvar-tracking',
+ '-fvar-tracking-assignments']
+endif
+
+# compilers
+cc = meson.get_compiler('c')
+cxx= meson.get_compiler('cpp')
+
+# extra arguments, if available
+foreach extra_arg : extra_flags
+ if cc.has_argument (extra_arg)
+ add_project_arguments([extra_arg], language: 'c')
+ endif
+ if cxx.has_argument (extra_arg)
+ add_project_arguments([extra_arg], language: 'cpp')
+ endif
+endforeach
+
+# some clang don't have charconv, but we need it.
+# https://github.com/djcb/mu/issues/2347
+cxx.check_header('charconv', required:true)
+
+################################################################################
+# config.h setup
+#
+config_h_data=configuration_data()
+config_h_data.set_quoted('MU_STORE_SCHEMA_VERSION', '465')
+config_h_data.set_quoted('PACKAGE_VERSION', meson.project_version())
+config_h_data.set_quoted('PACKAGE_STRING', meson.project_name() + ' ' +
+ meson.project_version())
+config_h_data.set_quoted('VERSION', meson.project_version())
+config_h_data.set_quoted('PACKAGE_NAME', meson.project_name())
+
+add_project_arguments(['-DHAVE_CONFIG_H'], language: 'c')
+add_project_arguments(['-DHAVE_CONFIG_H'], language: 'cpp')
+config_h_dep=declare_dependency(
+ include_directories: include_directories(['.']))
+
+functions=[
+ 'setsid'
+]
+foreach f : functions
+ if cc.has_function(f)
+ define = 'HAVE_' + f.underscorify().to_upper()
+ config_h_data.set(define, 1)
+ endif
+endforeach
+
+if cc.has_function('wordexp')
+ config_h_data.set('HAVE_WORDEXP_H',1)
+endif
+
+testmaildir=join_paths(meson.current_source_dir(), 'lib', 'tests')
+config_h_data.set_quoted('MU_TESTMAILDIR', join_paths(testmaildir, 'testdir'))
+config_h_data.set_quoted('MU_TESTMAILDIR2', join_paths(testmaildir, 'testdir2'))
+config_h_data.set_quoted('MU_TESTMAILDIR4', join_paths(testmaildir, 'testdir4'))
+config_h_data.set_quoted('MU_TESTMAILDIR_CJK', join_paths(testmaildir, 'cjk'))
+
+################################################################################
+# hard dependencies
+#
+glib_dep = dependency('glib-2.0', version: '>= 2.58')
+gobject_dep = dependency('gobject-2.0', version: '>= 2.58')
+gio_dep = dependency('gio-2.0', version: '>= 2.50')
+gmime_dep = dependency('gmime-3.0', version: '>= 3.2')
+xapian_dep = dependency('xapian-core', version:'>= 1.4')
+thread_dep = dependency('threads')
+
+awk=find_program(['gawk', 'awk'])
+gzip=find_program('gzip')
+
+# soft dependencies
+guile_dep = dependency('guile-3.0', required: get_option('guile'))
+# soft dependencies
+
+# emacs -- needed for mu4e compilation
+emacs_name=get_option('emacs')
+emacs=find_program([emacs_name], version: '>=25.3', required:false)
+if not emacs.found()
+ message('emacs not found; not pre-compiling mu4e sources')
+endif
+
+makeinfo=find_program(['makeinfo'], required:false)
+if not makeinfo.found()
+ message('makeinfo (texinfo) not found; not building info documentation')
+else
+ install_info=find_program(['install-info'], required:false)
+ if not install_info.found()
+ message('install-info not found')
+ else
+ install_info_script=join_paths(meson.current_source_dir(), 'build-aux',
+ 'meson-install-info.sh')
+ endif
+endif
+
+# readline. annoyingly, macos has an incompatible libedit claiming to be
+# readline. this is only a dev/debug convenience for the mu4e repl.
+readline_dep=[]
+if get_option('readline').enabled()
+ readline_dep = dependency('readline', version:'>= 8.0')
+ config_h_data.set('HAVE_LIBREADLINE', 1)
+ config_h_data.set('HAVE_READLINE_READLINE_H', 1)
+ config_h_data.set('HAVE_READLINE_HISTORY', 1)
+ config_h_data.set('HAVE_READLINE_HISTORY_H', 1)
+endif
+
+
+################################################################################
+# write out version.texi (for texiinfo builds in mu4e, guile)
+version_texi_data=configuration_data()
+version_texi_data.set('VERSION', meson.project_version())
+version_texi_data.set('EDITION', meson.project_version())
+version_texi_data.set('UPDATED',
+ run_command('date', '+%d %B %Y', check:true).stdout().strip())
+version_texi_data.set('UPDATEDMONTH',
+ run_command('date', '+%B %Y', check:true).stdout().strip())
+
+configure_file(input: 'version.texi.in',
+ output: 'version.texi',
+ configuration: version_texi_data)
+
+################################################################################
+# install some data files
+install_data('NEWS.org',
+ install_dir : join_paths(datadir,'doc', 'mu'))
+
+################################################################################
+# subdirs
+subdir('lib')
+subdir('mu')
+subdir('man')
+
+if emacs.found()
+ subdir('mu4e')
+endif
+
+if not get_option('guile').disabled() and guile_dep.found()
+ config_h_data.set('BUILD_GUILE', 1)
+ config_h_data.set_quoted('GUILE_BINARY',
+ guile_dep.get_pkgconfig_variable('guile'))
+ #message('guile is disabled for now')
+ subdir('guile')
+endif
+
+config_h_data.set_quoted('MU_PROGRAM', mu.full_path())
+################################################################################
+
+################################################################################
+# write-out config.h
+configure_file(output : 'config.h', configuration : config_h_data)
--- /dev/null
+## 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.
+
+
+option('guile',
+ type : 'feature',
+ value: 'auto',
+ description: 'build the guile scripting support (requires guile-3.x)')
+
+option('readline',
+ type: 'feature',
+ value: 'auto',
+ description: 'enable readline support for the mu4e repl')
+
+option('emacs',
+ type: 'string',
+ value: 'emacs',
+ description: 'name/path of the emacs executable')
+
+option('lispdir',
+ type: 'string',
+ description: 'path under which to install emacs-lisp files')
--- /dev/null
+## Copyright (C) 2010-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+AM_CPPFLAGS= \
+ -I${top_srcdir}/lib \
+ $(GLIB_CFLAGS) \
+ $(XAPIAN_CFLAGS) \
+ $(CODE_COVERAGE_CFLAGS)
+
+AM_CXXFLAGS= \
+ $(GMIME_CFLAGS) \
+ -DMU_SCRIPTS_DIR="\"$(pkgdatadir)/scripts/\"" \
+ $(ASAN_CXXCFLAGS) \
+ $(WARN_CXXFLAGS) \
+ $(CODE_COVERAGE_CFLAGS) \
+ -Wno-switch-enum
+
+AM_LDFLAGS= \
+ $(ASAN_LDFLAGS)
+
+bin_PROGRAMS= \
+ mu
+
+# note, mu.cc is only '.cc' and not '.c' because libmu must explicitly
+# be linked as c++, not c.
+mu_SOURCES= \
+ mu.cc \
+ mu-cmd-cfind.cc \
+ mu-config.cc \
+ mu-config.hh \
+ mu-cmd-extract.cc \
+ mu-cmd-find.cc \
+ mu-cmd-index.cc \
+ mu-cmd-server.cc \
+ mu-cmd-script.cc \
+ mu-cmd-fields.cc \
+ mu-cmd.cc \
+ mu-cmd.hh
+
+BUILT_SOURCES= \
+ mu-help-strings.inc
+
+mu-help-strings.inc: mu-help-strings.txt mu-help-strings.awk
+ $(AM_V_GEN) $(AWK) -f ${top_srcdir}/mu/mu-help-strings.awk < $< > $@
+
+mu_LDADD= \
+ ${top_builddir}/lib/libmu.la \
+ ${top_builddir}/lib/utils/libmu-utils.la \
+ $(GLIB_LIBS) \
+ $(XAPIAN_LIBS) \
+ $(READLINE_LIBS) \
+ $(CODE_COVERAGE_LIBS)
+
+EXTRA_DIST= \
+ mu-help-strings.awk \
+ mu-help-strings.txt
+
+CLEANFILES= \
+ $(BUILT_SOURCES)
--- /dev/null
+## 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.
+
+awk_script=join_paths(meson.current_source_dir(), 'mu-help-strings.awk')
+mu_help_strings_h=custom_target('mu_help',
+ input: 'mu-help-strings.txt',
+ output: 'mu-help-strings.inc',
+ command: [awk, '-f', awk_script, '@INPUT@'],
+ capture: true)
+mu = executable(
+ 'mu', [
+ 'mu.cc',
+ 'mu-cmd-cfind.cc',
+ 'mu-cmd-extract.cc',
+ 'mu-cmd-fields.cc',
+ 'mu-cmd-find.cc',
+ 'mu-cmd-index.cc',
+ 'mu-cmd-script.cc',
+ 'mu-cmd-server.cc',
+ 'mu-cmd.cc',
+ 'mu-cmd.hh',
+ 'mu-config.cc',
+ 'mu-config.hh',
+ mu_help_strings_h
+],
+ dependencies: [ glib_dep, gmime_dep, lib_mu_dep, thread_dep, config_h_dep ],
+ cpp_args: ['-DMU_SCRIPTS_DIR="'+ join_paths(datadir, 'mu', 'scripts') + '"'],
+ install: true)
+
+subdir('tests')
--- /dev/null
+/*
+** 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.
+**
+*/
+
+#include "config.h"
+
+#include <string>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "mu-cmd.hh"
+#include "mu-contacts-cache.hh"
+#include "mu-runtime.hh"
+
+#include "utils/mu-util.h"
+#include "utils/mu-utils.hh"
+#include "utils/mu-error.hh"
+
+using namespace Mu;
+
+/**
+ * macro to check whether the string is empty, ie. if it's NULL or
+ * it's length is 0
+ *
+ * @param S a string
+ *
+ * @return TRUE if the string is empty, FALSE otherwise
+ */
+#define mu_str_is_empty(S) ((!(S)||!(*S))?TRUE:FALSE)
+
+
+/**
+ * guess the last name for the given name; clearly,
+ * this is just a rough guess for setting an initial value.
+ *
+ * @param name a name
+ *
+ * @return the last name, as a newly allocated string (free with
+ * g_free)
+ */
+static gchar*
+guess_last_name(const char* name)
+{
+ const gchar* lastsp;
+
+ if (!name)
+ return g_strdup("");
+
+ lastsp = g_strrstr(name, " ");
+
+ return g_strdup(lastsp ? lastsp + 1 : "");
+}
+
+/**
+ * guess the first name for the given name; clearly,
+ * this is just a rough guess for setting an initial value.
+ *
+ * @param name a name
+ *
+ * @return the first name, as a newly allocated string (free with
+ * g_free)
+ */
+static gchar*
+guess_first_name(const char* name)
+{
+ const gchar* lastsp;
+
+ if (!name)
+ return g_strdup("");
+
+ lastsp = g_strrstr(name, " ");
+
+ if (lastsp)
+ return g_strndup(name, lastsp - name);
+ else
+ return g_strdup(name);
+}
+
+/**
+ * guess some nick name for the given name; if we can determine an
+ * first name, last name, the nick will be first name + the first char
+ * of the last name. otherwise, it's just the first name. clearly,
+ * this is just a rough guess for setting an initial value for nicks.
+ *
+ * @param name a name
+ *
+ * @return the guessed nick, as a newly allocated string (free with g_free)
+ */
+static gchar*
+cleanup_str(const char* str)
+{
+ gchar* s;
+ const gchar* cur;
+ unsigned i;
+
+ if (mu_str_is_empty(str))
+ return g_strdup("");
+
+ s = g_new0(char, strlen(str) + 1);
+
+ for (cur = str, i = 0; *cur; ++cur) {
+ if (ispunct(*cur) || isspace(*cur))
+ continue;
+ else
+ s[i++] = *cur;
+ }
+
+ return s;
+}
+
+static char*
+uniquify_nick(const char* nick, GHashTable* nicks)
+{
+ guint u;
+
+ for (u = 2; u != 1000; ++u) {
+ char* cand;
+ cand = g_strdup_printf("%s%u", nick, u);
+ if (!g_hash_table_contains(nicks, cand))
+ return cand;
+ }
+
+ return g_strdup(nick); /* if all else fails */
+}
+
+static gchar*
+guess_nick(const char* name, GHashTable* nicks)
+{
+ gchar *fname, *lname, *nick;
+ gchar initial[7];
+
+ fname = guess_first_name(name);
+ lname = guess_last_name(name);
+
+ /* if there's no last name, use first name as the nick */
+ if (mu_str_is_empty(fname) || mu_str_is_empty(lname)) {
+ g_free(lname);
+ nick = fname;
+ goto leave;
+ }
+
+ memset(initial, 0, sizeof(initial));
+ /* couldn't we get an initial for the last name? */
+ if (g_unichar_to_utf8(g_utf8_get_char(lname), initial) == 0) {
+ g_free(lname);
+ nick = fname;
+ goto leave;
+ }
+
+ nick = g_strdup_printf("%s%s", fname, initial);
+ g_free(fname);
+ g_free(lname);
+
+leave : {
+ gchar* tmp;
+ tmp = cleanup_str(nick);
+ g_free(nick);
+ nick = tmp;
+}
+
+ if (g_hash_table_contains(nicks, nick)) {
+ char* tmp;
+ tmp = uniquify_nick(nick, nicks);
+ g_free(nick);
+ nick = tmp;
+ }
+
+ g_hash_table_add(nicks, g_strdup(nick));
+
+ return nick;
+}
+
+static void
+print_header(const MuConfigFormat format)
+{
+ switch (format) {
+ case MU_CONFIG_FORMAT_BBDB:
+ g_print(";; -*-coding: utf-8-emacs;-*-\n"
+ ";;; file-version: 6\n");
+ break;
+ case MU_CONFIG_FORMAT_MUTT_AB:
+ g_print("Matching addresses in the mu database:\n");
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+each_contact_bbdb(const std::string& email, const std::string& name, time_t tstamp)
+{
+ char *fname, *lname;
+
+ fname = guess_first_name(name.c_str());
+ lname = guess_last_name(name.c_str());
+
+ const auto now{time_to_string("%Y-%m-%d", time(NULL))};
+ const auto timestamp{time_to_string("%Y-%m-%d", tstamp)};
+
+ g_print("[\"%s\" \"%s\" nil nil nil nil (\"%s\") "
+ "((creation-date . \"%s\") (time-stamp . \"%s\")) nil]\n",
+ fname,
+ lname,
+ email.c_str(),
+ now.c_str(),
+ timestamp.c_str());
+
+ g_free(fname);
+ g_free(lname);
+}
+
+static void
+each_contact_mutt_alias(const std::string& email,
+ const std::string& name,
+ GHashTable* nicks)
+{
+ if (name.empty())
+ return;
+
+ char* nick = guess_nick(name.c_str(), nicks);
+ mu_util_print_encoded("alias %s %s <%s>\n", nick, name.c_str(), email.c_str());
+
+ g_free(nick);
+}
+
+static void
+each_contact_wl(const std::string& email,
+ const std::string& name,
+ GHashTable* nicks)
+{
+ if (name.empty())
+ return;
+
+ char* nick = guess_nick(name.c_str(), nicks);
+ mu_util_print_encoded("%s \"%s\" \"%s\"\n", email.c_str(), nick, name.c_str());
+ g_free(nick);
+}
+
+static void
+print_plain(const std::string& email, const std::string& name, bool color)
+{
+ if (!name.empty()) {
+ if (color)
+ ::fputs(MU_COLOR_MAGENTA, stdout);
+ mu_util_fputs_encoded(name.c_str(), stdout);
+ ::fputs(" ", stdout);
+ }
+
+ if (color)
+ ::fputs(MU_COLOR_GREEN, stdout);
+
+ mu_util_fputs_encoded(email.c_str(), stdout);
+
+ if (color)
+ fputs(MU_COLOR_DEFAULT, stdout);
+
+ fputs("\n", stdout);
+}
+
+struct ECData {
+ MuConfigFormat format;
+ gboolean color, personal;
+ time_t after;
+ GRegex* rx;
+ GHashTable* nicks;
+ size_t maxnum;
+ size_t n;
+};
+
+static void
+each_contact(const Mu::Contact& ci, ECData& ecdata)
+{
+ if (ecdata.personal && !ci.personal)
+ return;
+
+ if (ci.message_date < ecdata.after)
+ return;
+
+ if (ecdata.rx &&
+ !g_regex_match(ecdata.rx, ci.email.c_str(), (GRegexMatchFlags)0, NULL) &&
+ !g_regex_match(ecdata.rx,
+ ci.name.empty() ? "" : ci.name.c_str(),
+ (GRegexMatchFlags)0,
+ NULL))
+ return;
+
+ ++ecdata.n;
+
+ switch (ecdata.format) {
+ case MU_CONFIG_FORMAT_MUTT_ALIAS:
+ each_contact_mutt_alias(ci.email, ci.name, ecdata.nicks);
+ break;
+ case MU_CONFIG_FORMAT_MUTT_AB:
+ mu_util_print_encoded("%s\t%s\t\n", ci.email.c_str(), ci.name.c_str());
+ break;
+ case MU_CONFIG_FORMAT_WL: each_contact_wl(ci.email, ci.name, ecdata.nicks);
+ break;
+ case MU_CONFIG_FORMAT_ORG_CONTACT:
+ if (!ci.name.empty())
+ mu_util_print_encoded("* %s\n:PROPERTIES:\n:EMAIL: %s\n:END:\n\n",
+ ci.name.c_str(),
+ ci.email.c_str());
+ break;
+ case MU_CONFIG_FORMAT_BBDB: each_contact_bbdb(ci.email, ci.name, ci.message_date);
+ break;
+ case MU_CONFIG_FORMAT_CSV:
+ mu_util_print_encoded("%s,%s\n",
+ ci.name.empty() ? "" : Mu::quote(ci.name).c_str(),
+ Mu::quote(ci.email).c_str());
+ break;
+ case MU_CONFIG_FORMAT_DEBUG: {
+ char datebuf[32];
+ const auto mdate(static_cast<::time_t>(ci.message_date));
+ ::strftime(datebuf, sizeof(datebuf), "%F %T", ::gmtime(&mdate));
+ g_print("%s\n\tname: %s\n\t%s\n\tpersonal: %s\n\tfreq: %zu\n"
+ "\tlast-seen: %s\n",
+ ci.email.c_str(),
+ ci.name.empty() ? "<none>" : ci.name.c_str(),
+ ci.display_name(true).c_str(),
+ ci.personal ? "yes" : "no",
+ ci.frequency,
+ datebuf);
+ }
+ break;
+ default:
+ print_plain(ci.email, ci.name, ecdata.color);
+ }
+}
+
+static Result<void>
+run_cmd_cfind(const Mu::Store& store,
+ const char* pattern,
+ gboolean personal,
+ time_t after,
+ int maxnum,
+ const MuConfigFormat format,
+ gboolean color)
+{
+ ECData ecdata{};
+ GError *err{};
+
+ memset(&ecdata, 0, sizeof(ecdata));
+
+ if (pattern) {
+ ecdata.rx = g_regex_new(
+ pattern,
+ (GRegexCompileFlags)(G_REGEX_CASELESS | G_REGEX_OPTIMIZE),
+ (GRegexMatchFlags)0, &err);
+
+ if (!ecdata.rx)
+ return Err(Error::Code::Internal, &err, "invalid cfind regexp");
+ }
+
+ ecdata.personal = personal;
+ ecdata.n = 0;
+ ecdata.after = after;
+ ecdata.maxnum = maxnum;
+ ecdata.format = format;
+ ecdata.color = color;
+ ecdata.nicks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+
+ print_header(format);
+
+ store.contacts_cache().for_each([&](const auto& ci) {
+ each_contact(ci, ecdata);
+ return ecdata.maxnum == 0 || ecdata.n < ecdata.maxnum;
+ });
+ g_hash_table_unref(ecdata.nicks);
+
+ if (ecdata.rx)
+ g_regex_unref(ecdata.rx);
+
+ if (ecdata.n == 0)
+ return Err(Error::Code::ContactNotFound, "no matching contacts found");
+ else
+ return Ok();
+}
+
+static gboolean
+cfind_params_valid(const MuConfig* opts)
+{
+ if (!opts || opts->cmd != MU_CONFIG_CMD_CFIND)
+ return FALSE;
+
+ switch (opts->format) {
+ case MU_CONFIG_FORMAT_PLAIN:
+ case MU_CONFIG_FORMAT_MUTT_ALIAS:
+ case MU_CONFIG_FORMAT_MUTT_AB:
+ case MU_CONFIG_FORMAT_WL:
+ case MU_CONFIG_FORMAT_BBDB:
+ case MU_CONFIG_FORMAT_CSV:
+ case MU_CONFIG_FORMAT_ORG_CONTACT:
+ case MU_CONFIG_FORMAT_DEBUG: break;
+ default:
+ g_printerr("invalid output format %s\n",
+ opts->formatstr ? opts->formatstr : "<none>");
+ return FALSE;
+ }
+
+ /* only one pattern allowed */
+ if (opts->params[1] && opts->params[2]) {
+ g_printerr("usage: mu cfind [options] [<ptrn>]\n");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+Result<void>
+Mu::mu_cmd_cfind(const Mu::Store& store, const MuConfig* opts)
+{
+ if (!cfind_params_valid(opts))
+ return Err(Error::Code::InvalidArgument, "error in parameters");
+ else
+ return run_cmd_cfind(store,
+ opts->params[1],
+ opts->personal,
+ opts->after,
+ opts->maxnum,
+ opts->format,
+ !opts->nocolor);
+}
--- /dev/null
+/*
+** Copyright (C) 2010-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-cmd.hh"
+#include "mu-config.hh"
+#include "utils/mu-util.h"
+#include "utils/mu-utils.hh"
+#include <message/mu-message.hh>
+#include <regex>
+
+using namespace Mu;
+
+
+static Result<void>
+save_part(const Message::Part& part, size_t idx, const MuConfig* opts)
+{
+ const auto targetdir = std::invoke([&]{
+ auto tdir{std::string{opts->targetdir ? opts->targetdir : ""}};
+ return tdir.empty() ? tdir : tdir + G_DIR_SEPARATOR_S;
+ });
+ const auto path{targetdir +
+ part.cooked_filename().value_or(format("part-%zu", idx))};
+
+ if (auto&& res{part.to_file(path, opts->overwrite)}; !res)
+ return Err(res.error());
+
+ if (opts->play) {
+ GError *err{};
+ if (auto res{mu_util_play(path.c_str(), &err)};
+ res != MU_OK)
+ return Err(Error::Code::Play, &err, "playing '%s' failed",
+ path.c_str());
+ }
+
+ return Ok();
+}
+
+static Result<void>
+save_parts(const std::string& path, Option<std::string>& filename_rx,
+ const MuConfig* opts)
+{
+ auto message{Message::make_from_path(path, mu_config_message_options(opts))};
+ if (!message)
+ return Err(std::move(message.error()));
+
+
+ size_t partnum{}, saved_num{};
+ const auto partnums = std::invoke([&]()->std::vector<size_t> {
+ std::vector<size_t> nums;
+ for (auto&& numstr : split(opts->parts ? opts->parts : "", ','))
+ nums.emplace_back(
+ static_cast<size_t>(::atoi(numstr.c_str())));
+ return nums;
+ });
+
+
+ for (auto&& part: message->parts()) {
+
+ ++partnum;
+
+ if (!opts->save_all) {
+
+ if (!partnums.empty() &&
+ !seq_some(partnums, [&](auto&& num){return num==partnum;}))
+ continue; // not a wanted partnum.
+
+ if (filename_rx && (!part.raw_filename() ||
+ !std::regex_match(*part.raw_filename(),
+ std::regex{*filename_rx})))
+ continue; // not a wanted pattern.
+ }
+
+ if (auto res = save_part(part, partnum, opts); !res)
+ return res;
+
+ ++saved_num;
+ }
+
+ // if (saved_num == 0)
+ // return Err(Error::Code::File,
+ // "no %s extracted from this message",
+ // opts->save_attachments ? "attachments" : "parts");
+ // else
+ return Ok();
+}
+
+#define color_maybe(C) \
+ do { \
+ if (color) \
+ fputs((C), stdout); \
+ } while (0)
+
+static void
+show_part(const MessagePart& part, size_t index, bool color)
+{
+ /* index */
+ g_print(" %zu ", index);
+
+ /* filename */
+ color_maybe(MU_COLOR_GREEN);
+ const auto fname{part.raw_filename()};
+ mu_util_fputs_encoded(fname ? fname->c_str() : "<none>", stdout);
+
+ mu_util_fputs_encoded(" ", stdout);
+
+ /* content-type */
+ color_maybe(MU_COLOR_BLUE);
+ const auto ctype{part.mime_type()};
+ mu_util_fputs_encoded(ctype ? ctype->c_str() : "<none>", stdout);
+
+ /* /\* disposition *\/ */
+ color_maybe(MU_COLOR_MAGENTA);
+ mu_util_print_encoded(" [%s]", part.is_attachment() ?
+ "attachment" : "inline");
+ /* size */
+ if (part.size() > 0) {
+ color_maybe(MU_COLOR_CYAN);
+ g_print(" (%zu bytes)", part.size());
+ }
+
+ color_maybe(MU_COLOR_DEFAULT);
+ fputs("\n", stdout);
+}
+
+static Mu::Result<void>
+show_parts(const char* path, const MuConfig* opts)
+{
+ //msgopts = mu_config_get_msg_options(opts);
+
+ auto msg_res{Message::make_from_path(path, mu_config_message_options(opts))};
+ if (!msg_res)
+ return Err(std::move(msg_res.error()));
+
+ /* TODO: update this for crypto */
+ size_t index{};
+ g_print("MIME-parts in this message:\n");
+ for (auto&& part: msg_res->parts())
+ show_part(part, ++index, !opts->nocolor);
+
+ return Ok();
+}
+
+static Mu::Result<void>
+check_params(const MuConfig* opts)
+{
+ size_t param_num;
+ param_num = mu_config_param_num(opts);
+
+ if (param_num < 2)
+ return Err(Error::Code::InvalidArgument, "parameters missing");
+
+ if (opts->save_attachments || opts->save_all)
+ if (opts->parts || param_num == 3)
+ return Err(Error::Code::User,
+ "--save-attachments and --save-all don't "
+ "accept a filename pattern or --parts");
+
+ if (opts->save_attachments && opts->save_all)
+ return Err(Error::Code::User,
+ "only one of --save-attachments and"
+ " --save-all is allowed");
+ return Ok();
+}
+
+
+Mu::Result<void>
+Mu::mu_cmd_extract(const MuConfig* opts)
+{
+ if (!opts || opts->cmd != MU_CONFIG_CMD_EXTRACT)
+ return Err(Error::Code::Internal, "error in arguments");
+ if (auto res = check_params(opts); !res)
+ return Err(std::move(res.error()));
+
+ if (!opts->params[2] && !opts->parts &&
+ !opts->save_attachments && !opts->save_all)
+ return show_parts(opts->params[1], opts); /* show, don't save */
+
+ if (!mu_util_check_dir(opts->targetdir, FALSE, TRUE))
+ return Err(Error::Code::File,
+ "target '%s' is not a writable directory",
+ opts->targetdir);
+
+ Option<std::string> pattern{};
+ if (opts->params[2])
+ pattern = opts->params[2];
+
+ return save_parts(opts->params[1], pattern, opts);
+}
--- /dev/null
+/*
+** 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 <iostream>
+#include <functional>
+
+#include "mu-cmd.hh"
+#include <message/mu-message.hh>
+#include "utils/mu-utils.hh"
+
+#include "thirdparty/tabulate.hpp"
+
+
+using namespace Mu;
+using namespace tabulate;
+
+
+static void
+table_header(Table& table, const MuConfig* opts)
+{
+ if (opts->nocolor)
+ return;
+
+ (*table.begin()).format()
+ .font_style({FontStyle::bold})
+ .font_color(Color::blue);
+
+}
+
+static void
+show_fields(const MuConfig* opts)
+{
+ using namespace std::string_literals;
+
+ Table fields;
+ fields.add_row({"field-name", "alias", "short", "search",
+ "value", "sexp", "example query", "description"});
+
+ auto disp= [&](std::string_view sv)->std::string {
+ if (sv.empty())
+ return "";
+ else
+ return format("%*s", STR_V(sv));
+ };
+
+ auto searchable=[&](const Field& field)->std::string {
+ if (field.is_boolean_term())
+ return "boolean";
+ if (field.is_indexable_term())
+ return "index";
+ if (field.is_normal_term())
+ return "yes";
+ if (field.is_contact())
+ return "contact";
+ if (field.is_range())
+ return "range";
+ return "no";
+ };
+
+ size_t row{};
+ field_for_each([&](auto&& field){
+ if (field.is_internal())
+ return; // skip.
+
+ fields.add_row({format("%*s", STR_V(field.name)),
+ field.alias.empty() ? "" : format("%*s", STR_V(field.alias)),
+ field.shortcut ? format("%c", field.shortcut) : ""s,
+ searchable(field),
+ field.is_value() ? "yes" : "no",
+ field.include_in_sexp() ? "yes" : "no",
+ disp(field.example_query),
+ disp(field.description)});
+ ++row;
+ });
+
+ table_header(fields, opts);
+
+ std::cout << fields << '\n';
+}
+
+static void
+show_flags(const MuConfig* opts)
+{
+ using namespace tabulate;
+ using namespace std::string_literals;
+
+ Table flags;
+ flags.add_row({"flag", "shortcut", "category", "description"});
+
+ flag_infos_for_each([&](const MessageFlagInfo& info) {
+
+ const auto catname = std::invoke(
+ [](MessageFlagCategory cat)->std::string {
+ switch(cat){
+ case MessageFlagCategory::Mailfile:
+ return "file";
+ case MessageFlagCategory::Maildir:
+ return "maildir";
+ case MessageFlagCategory::Content:
+ return "content";
+ case MessageFlagCategory::Pseudo:
+ return "pseudo";
+ default:
+ return {};
+ }
+ }, info.category);
+
+ flags.add_row({format("%*s", STR_V(info.name)),
+ format("%c", info.shortcut),
+ catname,
+ std::string{info.description}});
+ });
+
+ table_header(flags, opts);
+
+ std::cout << flags << '\n';
+}
+
+
+
+Result<void>
+Mu::mu_cmd_fields(const MuConfig* opts)
+{
+ g_return_val_if_fail(opts, Err(Error::Code::Internal, "no opts"));
+
+ if (!locale_workaround())
+ return Err(Error::Code::User, "failed to find a working locale");
+
+
+ std::cout << "#\n# message fields\n#\n";
+ show_fields(opts);
+ std::cout << "\n#\n# message flags\n#\n";
+ show_flags(opts);
+
+ return Ok();
+
+}
--- /dev/null
+/*
+** 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 <array>
+
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <signal.h>
+
+#include "message/mu-message.hh"
+#include "mu-maildir.hh"
+#include "mu-query-match-deciders.hh"
+#include "mu-query.hh"
+#include "mu-bookmarks.hh"
+#include "mu-runtime.hh"
+#include "message/mu-message.hh"
+
+#include "utils/mu-option.hh"
+#include "utils/mu-util.h"
+
+#include "mu-cmd.hh"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+struct OutputInfo {
+ Xapian::docid docid{};
+ bool header{};
+ bool footer{};
+ bool last{};
+ Option<QueryMatch&> match_info;
+};
+
+constexpr auto FirstOutput{OutputInfo{0, true, false, {}, {}}};
+constexpr auto LastOutput{OutputInfo{0, false, true, {}, {}}};
+
+using OutputFunc = std::function<bool(const Option<Message>& msg, const OutputInfo&,
+ const MuConfig*, GError**)>;
+
+static Result<void>
+print_internal(const Store& store,
+ const std::string& expr,
+ gboolean xapian,
+ gboolean warn)
+{
+ std::cout << store.parse_query(expr, xapian) << "\n";
+ return Ok();
+}
+
+static Result<QueryResults>
+run_query(const Store& store, const std::string& expr, const MuConfig* opts)
+{
+ const auto sortfield{field_from_name(opts->sortfield ? opts->sortfield : "")};
+ if (!sortfield && opts->sortfield)
+ return Err(Error::Code::InvalidArgument,
+ "invalid sort field: '%s'", opts->sortfield);
+
+ Mu::QueryFlags qflags{QueryFlags::SkipUnreadable};
+ if (opts->reverse)
+ qflags |= QueryFlags::Descending;
+ if (opts->skip_dups)
+ qflags |= QueryFlags::SkipDuplicates;
+ if (opts->include_related)
+ qflags |= QueryFlags::IncludeRelated;
+ if (opts->threads)
+ qflags |= QueryFlags::Threading;
+
+ return store.run_query(expr, sortfield.value_or(field_from_id(Field::Id::Date)).id,
+ qflags, opts->maxnum);
+}
+
+static gboolean
+exec_cmd(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+ if (!msg)
+ return TRUE;
+
+ gint status;
+ char * cmdline, *escpath;
+ gboolean rv;
+
+ escpath = g_shell_quote(msg->path().c_str());
+ cmdline = g_strdup_printf("%s %s", opts->exec, escpath);
+
+ rv = g_spawn_command_line_sync(cmdline, NULL, NULL, &status, err);
+
+ g_free(cmdline);
+ g_free(escpath);
+
+ return rv;
+}
+
+static gchar*
+resolve_bookmark(const MuConfig* opts, GError** err)
+{
+ MuBookmarks* bm;
+ char* val;
+ const gchar* bmfile;
+
+ bmfile = mu_runtime_path(MU_RUNTIME_PATH_BOOKMARKS);
+ bm = mu_bookmarks_new(bmfile);
+ if (!bm) {
+ g_set_error(err,
+ MU_ERROR_DOMAIN,
+ MU_ERROR_FILE_CANNOT_OPEN,
+ "failed to open bookmarks file '%s'",
+ bmfile);
+ return FALSE;
+ }
+
+ val = (gchar*)mu_bookmarks_lookup(bm, opts->bookmark);
+ if (!val)
+ g_set_error(err,
+ MU_ERROR_DOMAIN,
+ MU_ERROR_NO_MATCHES,
+ "bookmark '%s' not found",
+ opts->bookmark);
+ else
+ val = g_strdup(val);
+
+ mu_bookmarks_destroy(bm);
+ return val;
+}
+
+static Result<std::string>
+get_query(const MuConfig* opts)
+{
+ GError *err{};
+ gchar *query, *bookmarkval;
+
+ /* params[0] is 'find', actual search params start with [1] */
+ if (!opts->bookmark && !opts->params[1])
+ return Err(Error::Code::InvalidArgument, "error in parameters");
+
+ bookmarkval = {};
+ if (opts->bookmark) {
+ bookmarkval = resolve_bookmark(opts, &err);
+ if (!bookmarkval)
+ return Err(Error::Code::Command, &err,
+ "failed to resolve bookmark");
+ }
+
+ query = g_strjoinv(" ", &opts->params[1]);
+ if (bookmarkval) {
+ gchar* tmp;
+ tmp = g_strdup_printf("%s %s", bookmarkval, query);
+ g_free(query);
+ query = tmp;
+ }
+ g_free(bookmarkval);
+
+ return Ok(to_string_gchar(std::move(query)));
+}
+
+static bool
+prepare_links(const MuConfig* opts, GError** err)
+{
+ /* note, mu_maildir_mkdir simply ignores whatever part of the
+ * mail dir already exists */
+ if (auto&& res = maildir_mkdir(opts->linksdir, 0700, true); !res) {
+ res.error().fill_g_error(err);
+ return false;
+ }
+
+ if (!opts->clearlinks)
+ return false;
+
+ if (auto&& res = maildir_clear_links(opts->linksdir); !res) {
+ res.error().fill_g_error(err);
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+output_link(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+ if (info.header)
+ return prepare_links(opts, err);
+ else if (info.footer)
+ return true;
+
+ if (auto&& res = maildir_link(msg->path(), opts->linksdir); !res) {
+ res.error().fill_g_error(err);
+ return false;
+ }
+
+ return true;
+}
+
+static void
+ansi_color_maybe(Field::Id field_id, gboolean color)
+{
+ const char* ansi;
+
+ if (!color)
+ return; /* nothing to do */
+
+ switch (field_id) {
+ case Field::Id::From: ansi = MU_COLOR_CYAN; break;
+
+ case Field::Id::To:
+ case Field::Id::Cc:
+ case Field::Id::Bcc: ansi = MU_COLOR_BLUE; break;
+ case Field::Id::Subject: ansi = MU_COLOR_GREEN; break;
+ case Field::Id::Date: ansi = MU_COLOR_MAGENTA; break;
+
+ default:
+ if (field_from_id(field_id).type != Field::Type::String)
+ ansi = MU_COLOR_YELLOW;
+ else
+ ansi = MU_COLOR_RED;
+ }
+
+ fputs(ansi, stdout);
+}
+
+static void
+ansi_reset_maybe(Field::Id field_id, gboolean color)
+{
+ if (!color)
+ return; /* nothing to do */
+
+ fputs(MU_COLOR_DEFAULT, stdout);
+}
+
+static std::string
+display_field(const Message& msg, Field::Id field_id)
+{
+ switch (field_from_id(field_id).type) {
+ case Field::Type::String:
+ return msg.document().string_value(field_id);
+ case Field::Type::Integer:
+ if (field_id == Field::Id::Priority) {
+ return to_string(msg.priority());
+ } else if (field_id == Field::Id::Flags) {
+ return to_string(msg.flags());
+ } else /* as string */
+ return msg.document().string_value(field_id);
+ case Field::Type::TimeT:
+ return time_to_string(
+ "%c", static_cast<::time_t>(msg.document().integer_value(field_id)));
+ case Field::Type::ByteSize:
+ return to_string(msg.document().integer_value(field_id));
+ case Field::Type::StringList:
+ return join(msg.document().string_vec_value(field_id), ',');
+ case Field::Type::ContactList:
+ return to_string(msg.document().contacts_value(field_id));
+ default:
+ g_return_val_if_reached("");
+ return "";
+ }
+}
+
+static void
+print_summary(const Message& msg, const MuConfig* opts)
+{
+ const auto body{msg.body_text()};
+ if (!body)
+ return;
+
+ const auto summ{to_string_opt_gchar(
+ mu_str_summarize(body->c_str(),
+ opts->summary_len))};
+
+ g_print("Summary: ");
+ mu_util_fputs_encoded(summ ? summ->c_str() : "<none>", stdout);
+ g_print("\n");
+}
+
+static void
+thread_indent(const QueryMatch& info, const MuConfig* opts)
+{
+ const auto is_root{any_of(info.flags & QueryMatch::Flags::Root)};
+ const auto first_child{any_of(info.flags & QueryMatch::Flags::First)};
+ const auto last_child{any_of(info.flags & QueryMatch::Flags::Last)};
+ const auto empty_parent{any_of(info.flags & QueryMatch::Flags::Orphan)};
+ const auto is_dup{any_of(info.flags & QueryMatch::Flags::Duplicate)};
+ // const auto is_related{any_of(info.flags & QueryMatch::Flags::Related)};
+
+ /* indent */
+ if (opts->debug) {
+ ::fputs(info.thread_path.c_str(), stdout);
+ ::fputs(" ", stdout);
+ } else
+ for (auto i = info.thread_level; i > 1; --i)
+ ::fputs(" ", stdout);
+
+ if (!is_root) {
+ if (first_child)
+ ::fputs("\\", stdout);
+ else if (last_child)
+ ::fputs("/", stdout);
+ else
+ ::fputs(" ", stdout);
+ ::fputs(empty_parent ? "*> " : is_dup ? "=> "
+ : "-> ",
+ stdout);
+ }
+}
+
+static void
+output_plain_fields(const Message& msg, const char* fields,
+ gboolean color, gboolean threads)
+{
+ const char* myfields;
+ int nonempty;
+
+ g_return_if_fail(fields);
+
+ for (myfields = fields, nonempty = 0; *myfields; ++myfields) {
+ const auto field_opt{field_from_shortcut(*myfields)};
+ if (!field_opt || (!field_opt->is_value() && !field_opt->is_contact()))
+ nonempty += printf("%c", *myfields);
+
+ else {
+ ansi_color_maybe(field_opt->id, color);
+ nonempty += mu_util_fputs_encoded(
+ display_field(msg, field_opt->id).c_str(), stdout);
+ ansi_reset_maybe(field_opt->id, color);
+ }
+ }
+
+ if (nonempty)
+ fputs("\n", stdout);
+}
+
+static gboolean
+output_plain(const Option<Message>& msg, const OutputInfo& info,
+ const MuConfig* opts, GError** err)
+{
+ if (!msg)
+ return true;
+
+ /* we reuse the color (whatever that may be)
+ * for message-priority for threads, too */
+ ansi_color_maybe(Field::Id::Priority, !opts->nocolor);
+ if (opts->threads && info.match_info)
+ thread_indent(*info.match_info, opts);
+
+ output_plain_fields(*msg, opts->fields, !opts->nocolor, opts->threads);
+
+ if (opts->summary_len > 0)
+ print_summary(*msg, opts);
+
+ return TRUE;
+}
+
+static bool
+output_sexp(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+ if (msg) {
+
+ if (const auto sexp{msg->cached_sexp()}; !sexp.empty())
+ fputs(sexp.c_str(), stdout);
+ else
+ fputs(msg->to_sexp().to_sexp_string().c_str(), stdout);
+
+ fputs("\n", stdout);
+ }
+
+ return true;
+}
+
+static bool
+output_json(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+ if (info.header) {
+ g_print("[\n");
+ return true;
+ }
+
+ if (info.footer) {
+ g_print("]\n");
+ return true;
+ }
+
+ if (!msg)
+ return true;
+
+ g_print("%s%s\n",
+ msg->to_sexp().to_json_string().c_str(),
+ info.last ? "" : ",");
+
+ return true;
+}
+
+static void
+print_attr_xml(const std::string& elm, const std::string& str)
+{
+ if (str.empty())
+ return; /* empty: don't include */
+
+ auto&& esc{to_string_opt_gchar(g_markup_escape_text(str.c_str(), -1))};
+ g_print("\t\t<%s>%s</%s>\n", elm.c_str(), esc.value_or("").c_str(), elm.c_str());
+}
+
+static bool
+output_xml(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+ if (info.header) {
+ g_print("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
+ g_print("<messages>\n");
+ return true;
+ }
+
+ if (info.footer) {
+ g_print("</messages>\n");
+ return true;
+ }
+
+ g_print("\t<message>\n");
+ print_attr_xml("from", to_string(msg->from()));
+ print_attr_xml("to", to_string(msg->to()));
+ print_attr_xml("cc", to_string(msg->cc()));
+ print_attr_xml("subject", msg->subject());
+ g_print("\t\t<date>%u</date>\n", (unsigned)msg->date());
+ g_print("\t\t<size>%u</size>\n", (unsigned)msg->size());
+ print_attr_xml("msgid", msg->message_id());
+ print_attr_xml("path", msg->path());
+ print_attr_xml("maildir", msg->maildir());
+ g_print("\t</message>\n");
+
+ return true;
+}
+
+static OutputFunc
+get_output_func(const MuConfig* opts, GError** err)
+{
+ switch (opts->format) {
+ case MU_CONFIG_FORMAT_LINKS: return output_link;
+ case MU_CONFIG_FORMAT_EXEC: return exec_cmd;
+ case MU_CONFIG_FORMAT_PLAIN: return output_plain;
+ case MU_CONFIG_FORMAT_XML: return output_xml;
+ case MU_CONFIG_FORMAT_SEXP: return output_sexp;
+ case MU_CONFIG_FORMAT_JSON: return output_json;
+
+ default: g_return_val_if_reached(NULL); return NULL;
+ }
+}
+
+static Result<void>
+output_query_results(const QueryResults& qres, const MuConfig* opts)
+{
+ GError* err{};
+ const auto output_func{get_output_func(opts, &err)};
+ if (!output_func)
+ return Err(Error::Code::Query, &err, "failed to find output function");
+
+ gboolean rv{true};
+ output_func(Nothing, FirstOutput, opts, {});
+
+ size_t n{0};
+ for (auto&& item : qres) {
+ n++;
+ auto msg{item.message()};
+ if (!msg)
+ continue;
+
+ if (opts->after != 0 && msg->changed() < opts->after)
+ continue;
+
+ rv = output_func(msg,
+ {item.doc_id(),
+ false,
+ false,
+ n == qres.size(), /* last? */
+ item.query_match()},
+ opts,
+ &err);
+ if (!rv)
+ break;
+ }
+ output_func(Nothing, LastOutput, opts, {});
+
+
+ if (rv)
+ return Ok();
+ else
+ return Err(Error::Code::Query, &err, "error in query results output");
+}
+
+static Result<void>
+process_query(const Store& store, const std::string& expr, const MuConfig* opts)
+{
+ auto qres{run_query(store, expr, opts)};
+ if (!qres)
+ return Err(qres.error());
+
+ if (qres->empty())
+ return Err(Error::Code::NoMatches, "no matches for search expression");
+
+ return output_query_results(*qres, opts);
+}
+
+static Result<void>
+execute_find(const Store& store, const MuConfig* opts)
+{
+ auto expr{get_query(opts)};
+ if (!expr)
+ return Err(expr.error());
+
+ if (opts->format == MU_CONFIG_FORMAT_XQUERY)
+ return print_internal(store, *expr, TRUE, FALSE);
+ else if (opts->format == MU_CONFIG_FORMAT_MQUERY)
+ return print_internal(store, *expr, FALSE, opts->verbose);
+ else
+ return process_query(store, *expr, opts);
+}
+
+static gboolean
+format_params_valid(const MuConfig* opts, GError** err)
+{
+ switch (opts->format) {
+ case MU_CONFIG_FORMAT_EXEC: break;
+ case MU_CONFIG_FORMAT_PLAIN:
+ case MU_CONFIG_FORMAT_SEXP:
+ case MU_CONFIG_FORMAT_JSON:
+ case MU_CONFIG_FORMAT_LINKS:
+ case MU_CONFIG_FORMAT_XML:
+ case MU_CONFIG_FORMAT_XQUERY:
+ case MU_CONFIG_FORMAT_MQUERY:
+ if (opts->exec) {
+ mu_util_g_set_error(err,
+ MU_ERROR_IN_PARAMETERS,
+ "--exec and --format cannot be combined");
+ return FALSE;
+ }
+ break;
+ default:
+ mu_util_g_set_error(err,
+ MU_ERROR_IN_PARAMETERS,
+ "invalid output format %s",
+ opts->formatstr ? opts->formatstr : "<none>");
+ return FALSE;
+ }
+
+ if (opts->format == MU_CONFIG_FORMAT_LINKS && !opts->linksdir) {
+ mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS, "missing --linksdir argument");
+ return FALSE;
+ }
+
+ if (opts->linksdir && opts->format != MU_CONFIG_FORMAT_LINKS) {
+ mu_util_g_set_error(err,
+ MU_ERROR_IN_PARAMETERS,
+ "--linksdir is only valid with --format=links");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+query_params_valid(const MuConfig* opts, GError** err)
+{
+ const gchar* xpath;
+
+ if (!opts->params[1]) {
+ mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS, "missing query");
+ return FALSE;
+ }
+
+ xpath = mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB);
+ if (mu_util_check_dir(xpath, TRUE, FALSE))
+ return TRUE;
+
+ mu_util_g_set_error(err,
+ MU_ERROR_FILE_CANNOT_READ,
+ "'%s' is not a readable Xapian directory",
+ xpath);
+ return FALSE;
+}
+
+Result<void>
+Mu::mu_cmd_find(const Store& store, const MuConfig* opts)
+{
+ g_return_val_if_fail(opts, Err(Error::Code::Internal, "no opts"));
+ g_return_val_if_fail(opts->cmd == MU_CONFIG_CMD_FIND, Err(Error::Code::Internal,
+ "wrong command"));
+ MuConfig myopts{*opts};
+
+ if (myopts.exec)
+ myopts.format = MU_CONFIG_FORMAT_EXEC; /* pseudo format */
+
+ GError *err{};
+ if (!query_params_valid(&myopts, &err) || !format_params_valid(&myopts, &err))
+ return Err(Error::Code::InvalidArgument, &err, "invalid argument");
+ else
+ return execute_find(store, &myopts);
+}
--- /dev/null
+/*
+** 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 "mu-cmd.hh"
+
+#include <chrono>
+#include <thread>
+#include <atomic>
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include "index/mu-indexer.hh"
+#include "mu-store.hh"
+#include "mu-runtime.hh"
+
+#include "utils/mu-util.h"
+
+using namespace Mu;
+
+static std::atomic<bool> caught_signal;
+
+static void
+sig_handler(int _sig)
+{
+ caught_signal = true;
+}
+
+static void
+install_sig_handler(void)
+{
+ struct sigaction action;
+ int i, sigs[] = {SIGINT, SIGHUP, SIGTERM};
+
+ sigemptyset(&action.sa_mask);
+ action.sa_flags = SA_RESETHAND;
+ action.sa_handler = sig_handler;
+
+ for (i = 0; i != G_N_ELEMENTS(sigs); ++i)
+ if (sigaction(sigs[i], &action, NULL) != 0)
+ g_critical("set sigaction for %d failed: %s",
+ sigs[i], g_strerror(errno));
+}
+
+static void
+print_stats(const Indexer::Progress& stats, bool color)
+{
+ const char* kars = "-\\|/";
+ static auto i = 0U;
+
+ MaybeAnsi col{color};
+ using Color = MaybeAnsi::Color;
+
+ std::cout << col.fg(Color::Yellow) << kars[++i % 4] << col.reset() << " indexing messages; "
+ << "checked: " << col.fg(Color::Green) << stats.checked << col.reset()
+ << "; updated/new: " << col.fg(Color::Green) << stats.updated << col.reset()
+ << "; cleaned-up: " << col.fg(Color::Green) << stats.removed << col.reset();
+}
+
+Result<void>
+Mu::mu_cmd_index(Mu::Store& store, const MuConfig* opts)
+{
+ if (!opts || opts->cmd != MU_CONFIG_CMD_INDEX || opts->params[1])
+ return Err(Error::Code::InvalidArgument, "error in parameters");
+
+ if (opts->max_msg_size < 0)
+ return Err(Error::Code::InvalidArgument,
+ "the maximum message size must be >= 0");
+
+ const auto mdir{store.properties().root_maildir};
+ if (G_UNLIKELY(access(mdir.c_str(), R_OK) != 0))
+ return Err(Error::Code::File, "'%s' is not readable: %s",
+ mdir.c_str(), g_strerror(errno));
+
+ MaybeAnsi col{!opts->nocolor};
+ using Color = MaybeAnsi::Color;
+ if (!opts->quiet) {
+ if (opts->lazycheck)
+ std::cout << "lazily ";
+
+ std::cout << "indexing maildir " << col.fg(Color::Green)
+ << store.properties().root_maildir << col.reset() << " -> store "
+ << col.fg(Color::Green) << store.properties().database_path << col.reset()
+ << std::endl;
+ }
+
+ Mu::Indexer::Config conf{};
+ conf.cleanup = !opts->nocleanup;
+ conf.lazy_check = opts->lazycheck;
+ // ignore .noupdate with an empty store.
+ conf.ignore_noupdate = store.empty();
+
+ install_sig_handler();
+
+ auto& indexer{store.indexer()};
+ indexer.start(conf);
+ while (!caught_signal && indexer.is_running()) {
+ if (!opts->quiet)
+ print_stats(indexer.progress(), !opts->nocolor);
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
+
+ if (!opts->quiet) {
+ std::cout << "\r";
+ std::cout.flush();
+ }
+ }
+
+ store.indexer().stop();
+
+ if (!opts->quiet) {
+ print_stats(store.indexer().progress(), !opts->nocolor);
+ std::cout << std::endl;
+ }
+
+ return Ok();
+}
--- /dev/null
+/*
+** Copyright (C) 2012-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <errno.h>
+
+#include "mu-cmd.hh"
+#include "mu-script.hh"
+#include "mu-runtime.hh"
+
+#include "utils/mu-util.h"
+
+#define MU_GUILE_EXT ".scm"
+#define MU_GUILE_DESCR_PREFIX ";; INFO: "
+
+#define COL(C) ((color) ? C : "")
+
+using namespace Mu;
+
+static void
+print_script(const char* name,
+ const char* oneline,
+ const char* descr,
+ gboolean color,
+ gboolean verbose)
+{
+ g_print("%s%s%s%s%s%s%s%s",
+ verbose ? "\n" : " * ",
+ COL(MU_COLOR_GREEN),
+ name,
+ COL(MU_COLOR_DEFAULT),
+ oneline ? ": " : "",
+ COL(MU_COLOR_BLUE),
+ oneline ? oneline : "",
+ MU_COLOR_DEFAULT);
+
+ if (verbose && descr)
+ g_print("%s%s%s", COL(MU_COLOR_MAGENTA), descr, COL(MU_COLOR_DEFAULT));
+}
+
+static gboolean
+print_scripts(GSList* scripts, gboolean color, gboolean verbose, const char* rxstr, GError** err)
+{
+ GSList* cur;
+ const char* verb;
+
+ if (!scripts) {
+ g_print("No scripts available\n");
+ return TRUE; /* not an error */
+ }
+
+ verb = verbose ? "" : " (use --verbose for details)";
+
+ if (rxstr)
+ g_print("Available scripts matching '%s'%s:\n", rxstr, verb);
+ else
+ g_print("Available scripts%s:\n", verb);
+
+ for (cur = scripts; cur; cur = g_slist_next(cur)) {
+ MuScriptInfo* msi;
+ const char * descr, *oneline, *name;
+
+ msi = (MuScriptInfo*)cur->data;
+ name = mu_script_info_name(msi);
+ oneline = mu_script_info_one_line(msi);
+ descr = mu_script_info_description(msi);
+
+ /* if rxstr is provide, only consider matching scriptinfos */
+ if (rxstr && !mu_script_info_matches_regex(msi, rxstr, err)) {
+ if (err && *err)
+ return FALSE;
+ continue;
+ }
+
+ print_script(name, oneline, descr, color, verbose);
+ }
+
+ return TRUE;
+}
+
+static char*
+get_userpath(const char* muhome)
+{
+ if (muhome)
+ return g_build_path(G_DIR_SEPARATOR_S, muhome, "scripts", NULL);
+ else
+ return g_build_path(G_DIR_SEPARATOR_S,
+ g_get_user_data_dir(),
+ "mu",
+ "scripts",
+ NULL);
+}
+
+static GSList*
+get_script_info_list(const char* muhome, GError** err)
+{
+ GSList *scripts, *userscripts, *last;
+ char* userpath;
+
+ scripts = mu_script_get_script_info_list(MU_SCRIPTS_DIR,
+ MU_GUILE_EXT,
+ MU_GUILE_DESCR_PREFIX,
+ err);
+
+ if (err && *err)
+ return NULL;
+
+ userpath = get_userpath(muhome);
+
+ /* is there are userdir for scripts? */
+ if (!mu_util_check_dir(userpath, TRUE, FALSE)) {
+ g_free(userpath);
+ return scripts;
+ }
+
+ /* append it to the list we already have */
+ userscripts =
+ mu_script_get_script_info_list(userpath, MU_GUILE_EXT, MU_GUILE_DESCR_PREFIX, err);
+ g_free(userpath);
+
+ /* some error, return nothing */
+ if (err && *err) {
+ mu_script_info_list_destroy(userscripts);
+ mu_script_info_list_destroy(scripts);
+ return NULL;
+ }
+
+ /* append the user scripts */
+ last = g_slist_last(scripts);
+ if (last) {
+ last->next = userscripts;
+ return scripts;
+ } else
+ return userscripts; /* apparently, scripts was NULL */
+}
+
+Mu::Result<void>
+Mu::mu_cmd_script(const MuConfig* opts)
+{
+ GError *err{};
+ MuScriptInfo* msi;
+ GSList* scripts;
+
+ if (!mu_util_supports(MU_FEATURE_GUILE))
+ return Err(Error::Code::InvalidArgument,
+ "<script> sub-command not available (requires guile)");
+
+ scripts = get_script_info_list(opts->muhome, &err);
+ if (err)
+ goto leave;
+
+ if (g_strcmp0(opts->cmdstr, "script") == 0) {
+ print_scripts(scripts, !opts->nocolor, opts->verbose,
+ opts->script_params[0], &err);
+ goto leave;
+ }
+
+ msi = mu_script_find_script_with_name(scripts, opts->script);
+ if (!msi) {
+ mu_util_g_set_error(&err, MU_ERROR_SCRIPT_NOT_FOUND,
+ "command or script not found");
+ goto leave;
+ }
+
+ /* do it! */
+ mu_script_guile_run(msi, mu_runtime_path(MU_RUNTIME_PATH_CACHE),
+ opts->script_params, &err);
+leave:
+ /* this won't be reached, unless there is some error */
+ mu_script_info_list_destroy(scripts);
+
+ if (err)
+ return Err(Error::Code::InvalidArgument, &err,
+ "error running script");
+ else
+ return Ok();
+}
--- /dev/null
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <string>
+#include <algorithm>
+#include <atomic>
+#include <cstdio>
+
+#include <unistd.h>
+
+#include "mu-runtime.hh"
+#include "mu-cmd.hh"
+#include "mu-server.hh"
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-command-parser.hh"
+#include "utils/mu-readline.hh"
+
+using namespace Mu;
+static std::atomic<int> MuTerminate{0};
+static bool tty;
+
+static void
+sig_handler(int sig)
+{
+ MuTerminate = sig;
+}
+
+static void
+install_sig_handler(void)
+{
+ static struct sigaction action;
+ int i, sigs[] = {SIGINT, SIGHUP, SIGTERM, SIGPIPE};
+
+ MuTerminate = 0;
+
+ action.sa_handler = sig_handler;
+ sigemptyset(&action.sa_mask);
+ action.sa_flags = SA_RESETHAND;
+
+ for (i = 0; i != G_N_ELEMENTS(sigs); ++i)
+ if (sigaction(sigs[i], &action, NULL) != 0)
+ g_critical("set sigaction for %d failed: %s",
+ sigs[i], g_strerror(errno));
+}
+
+/*
+ * Markers for/after the length cookie that precedes the expression we write to
+ * output. We use octal 376, 377 (ie, 0xfe, 0xff) as they will never occur in
+ * utf8 */
+
+#define COOKIE_PRE "\376"
+#define COOKIE_POST "\377"
+
+static void
+cookie(size_t n)
+{
+ const auto num{static_cast<unsigned>(n)};
+
+ if (tty) // for testing.
+ ::printf("[%x]", num);
+ else
+ ::printf(COOKIE_PRE "%x" COOKIE_POST, num);
+}
+
+static void
+output_sexp_stdout(Sexp&& sexp, Server::OutputFlags flags)
+{
+ /* if requested, insert \n between list elements; note:
+ * is _not_ inherited by children */
+ if (any_of(flags & Server::OutputFlags::SplitList))
+ sexp.formatting_opts |= Sexp::FormattingOptions::SplitList;
+
+ const auto str{sexp.to_sexp_string()};
+
+ cookie(str.size() + 1);
+ if (G_UNLIKELY(::puts(str.c_str()) < 0)) {
+ g_critical("failed to write output '%s'", str.c_str());
+ ::raise(SIGTERM); /* terminate ourselves */
+ }
+
+ if (any_of(flags & Server::OutputFlags::Flush))
+ std::fflush(stdout);
+}
+
+static void
+report_error(const Mu::Error& err) noexcept
+{
+ Sexp::List e;
+
+ e.add_prop(":error", Sexp::make_number(static_cast<size_t>(err.code())));
+ e.add_prop(":message", Sexp::make_string(err.what()));
+
+ output_sexp_stdout(Sexp::make_list(std::move(e)),
+ Server::OutputFlags::Flush);
+}
+
+
+Result<void>
+Mu::mu_cmd_server(const MuConfig* opts) try {
+
+ auto store = Store::make(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB),
+ Store::Options::Writable);
+ if (!store)
+ return Err(store.error());
+
+ Server server{*store, output_sexp_stdout};
+ g_message("created server with store @ %s; maildir @ %s; debug-mode %s"
+ "readline: %s",
+ store->properties().database_path.c_str(),
+ store->properties().root_maildir.c_str(),
+ opts->debug ? "yes" : "no",
+ have_readline() ? "yes" : "no");
+
+ tty = ::isatty(::fileno(stdout));
+ const auto eval = std::string{opts->commands ? "(help :full t)"
+ : opts->eval ? opts->eval
+ : ""};
+ if (!eval.empty()) {
+ server.invoke(eval);
+ return Ok();
+ }
+
+ // Note, the readline stuff is inactive unless on a tty.
+ const auto histpath{std::string{mu_runtime_path(MU_RUNTIME_PATH_CACHE)} + "/history"};
+ setup_readline(histpath, 50);
+
+ install_sig_handler();
+ std::cout << ";; Welcome to the " << PACKAGE_STRING << " command-server\n"
+ << ";; Use (help) to get a list of commands, (quit) to quit.\n";
+
+ bool do_quit{};
+ while (!MuTerminate && !do_quit) {
+ std::fflush(stdout); // Needed for Windows, see issue #1827.
+ const auto line{read_line(do_quit)};
+ if (line.find_first_not_of(" \t") == std::string::npos)
+ continue; // skip whitespace-only lines
+
+ do_quit = server.invoke(line) ? false : true;
+ save_line(line);
+ }
+
+ if (MuTerminate != 0)
+ g_message ("shutting down due to signal %d", MuTerminate.load());
+
+ shutdown_readline();
+
+ return Ok();
+
+} catch (const Error& er) {
+ /* note: user-level error, "OK" for mu */
+ report_error(er);
+ g_warning("server caught exception: %s", er.what());
+ return Ok();
+} catch (...) {
+ g_critical("server caught exception");
+ return Err(Error::Code::Internal, "caught exception");
+}
--- /dev/null
+/*
+** 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 "config.h"
+
+#include <iostream>
+#include <iomanip>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "mu-config.hh"
+#include "mu-cmd.hh"
+#include "mu-maildir.hh"
+#include "mu-contacts-cache.hh"
+#include "mu-runtime.hh"
+#include "message/mu-message.hh"
+#include "message/mu-mime-object.hh"
+
+#include "utils/mu-util.h"
+
+#include "utils/mu-error.hh"
+#include "utils/mu-utils.hh"
+#include "message/mu-message.hh"
+
+#include <thirdparty/tabulate.hpp>
+
+#define VIEW_TERMINATOR '\f' /* form-feed */
+
+using namespace Mu;
+
+static Mu::Result<void>
+view_msg_sexp(const Message& message, const MuConfig* opts)
+{
+ ::fputs(message.to_sexp().to_sexp_string().c_str(), stdout);
+ ::fputs("\n", stdout);
+
+ return Ok();
+}
+
+
+static std::string /* return comma-sep'd list of attachments */
+get_attach_str(const Message& message, const MuConfig* opts)
+{
+ std::string str;
+ seq_for_each(message.parts(), [&](auto&& part) {
+ if (auto fname = part.raw_filename(); fname) {
+ if (str.empty())
+ str = fname.value();
+ else
+ str += ", " + fname.value();
+ }
+ });
+
+ return str;
+}
+
+#define color_maybe(C) \
+ do { \
+ if (color) \
+ fputs((C), stdout); \
+ } while (0)
+
+static void
+print_field(const std::string& field, const std::string& val, bool color)
+{
+ if (val.empty())
+ return;
+
+ color_maybe(MU_COLOR_MAGENTA);
+ mu_util_fputs_encoded(field.c_str(), stdout);
+ color_maybe(MU_COLOR_DEFAULT);
+ fputs(": ", stdout);
+
+ color_maybe(MU_COLOR_GREEN);
+ mu_util_fputs_encoded(val.c_str(), stdout);
+
+ color_maybe(MU_COLOR_DEFAULT);
+ fputs("\n", stdout);
+}
+
+/* a summary_len of 0 mean 'don't show summary, show body */
+static void
+body_or_summary(const Message& message, const MuConfig* opts)
+{
+ gboolean color;
+ //int my_opts = mu_config_get_msg_options(opts) | MU_MSG_OPTION_CONSOLE_PASSWORD;
+
+ color = !opts->nocolor;
+
+ const auto body{message.body_text()};
+ if (!body || body->empty()) {
+ if (any_of(message.flags() & Flags::Encrypted)) {
+ color_maybe(MU_COLOR_CYAN);
+ g_print("[No text body found; "
+ "message has encrypted parts]\n");
+ } else {
+ color_maybe(MU_COLOR_MAGENTA);
+ g_print("[No text body found]\n");
+ }
+ color_maybe(MU_COLOR_DEFAULT);
+ return;
+ }
+
+ if (opts->summary_len != 0) {
+ gchar* summ;
+ summ = mu_str_summarize(body->c_str(), opts->summary_len);
+ print_field("Summary", summ, color);
+ g_free(summ);
+ } else {
+ mu_util_print_encoded("%s", body->c_str());
+ if (!g_str_has_suffix(body->c_str(), "\n"))
+ g_print("\n");
+ }
+}
+
+/* we ignore fields for now */
+/* summary_len == 0 means "no summary */
+static Mu::Result<void>
+view_msg_plain(const Message& message, const MuConfig* opts)
+{
+ const auto color{!opts->nocolor};
+
+ print_field("From", to_string(message.from()), color);
+ print_field("To", to_string(message.to()), color);
+ print_field("Cc", to_string(message.cc()), color);
+ print_field("Bcc", to_string(message.bcc()), color);
+ print_field("Subject", message.subject(), color);
+
+ if (auto&& date = message.date(); date != 0)
+ print_field("Date", time_to_string("%c", date), color);
+
+ print_field("Tags", join(message.tags(), ", "), color);
+
+ print_field("Attachments",get_attach_str(message, opts), color);
+ body_or_summary(message, opts);
+
+ return Ok();
+}
+
+static Mu::Result<void>
+handle_msg(const std::string& fname, const MuConfig* opts)
+{
+ auto message{Message::make_from_path(fname, mu_config_message_options(opts))};
+ if (!message)
+ return Err(message.error());
+
+ switch (opts->format) {
+ case MU_CONFIG_FORMAT_PLAIN:
+ return view_msg_plain(*message, opts);
+ case MU_CONFIG_FORMAT_SEXP:
+ return view_msg_sexp(*message, opts);
+ default:
+ g_critical("bug: should not be reached");
+ return Err(Error::Code::Internal, "error");
+ }
+}
+
+static Mu::Result<void>
+view_params_valid(const MuConfig* opts)
+{
+ /* note: params[0] will be 'view' */
+ if (!opts->params[0] || !opts->params[1])
+ return Err(Error::Code::InvalidArgument, "error in parameters");
+
+ switch (opts->format) {
+ case MU_CONFIG_FORMAT_PLAIN:
+ case MU_CONFIG_FORMAT_SEXP: break;
+ default:
+ return Err(Error::Code::InvalidArgument, "invalid output format");
+ }
+
+ return Ok();
+}
+
+static Mu::Result<void>
+cmd_view(const MuConfig* opts)
+{
+ if (!opts || opts->cmd != Mu::MU_CONFIG_CMD_VIEW)
+ return Err(Error::Code::InvalidArgument, "invalid parameters");
+ if (auto res = view_params_valid(opts); !res)
+ return res;
+
+ for (auto i = 1; opts->params[i]; ++i) {
+ if (auto res = handle_msg(opts->params[i], opts); !res)
+ return res;
+ /* add a separator between two messages? */
+ if (opts->terminator)
+ g_print("%c", VIEW_TERMINATOR);
+ }
+
+ return Ok();
+}
+
+static Mu::Result<void>
+cmd_mkdir(const MuConfig* opts)
+{
+ int i;
+
+ if (!opts->params[1])
+ return Err(Error::Code::InvalidArgument,
+ "missing directory parameter");
+
+ for (i = 1; opts->params[i]; ++i) {
+ if (auto&& res =
+ maildir_mkdir(opts->params[i], opts->dirmode, FALSE); !res)
+ return res;
+ }
+
+ return Ok();
+}
+
+static Result<void>
+cmd_add(Mu::Store& store, const MuConfig* opts)
+{
+ /* note: params[0] will be 'add' */
+ if (!opts->params[0] || !opts->params[1])
+ return Err(Error::Code::InvalidArgument,
+ "expected some files to add");
+
+ for (auto u = 1; opts->params[u]; ++u) {
+
+ const auto docid{store.add_message(opts->params[u])};
+ if (!docid)
+ return Err(docid.error());
+ else
+ g_debug("added message @ %s, docid=%u",
+ opts->params[u], docid.value());
+ }
+
+ return Ok();
+}
+
+static Result<void>
+cmd_remove(Mu::Store& store, const MuConfig* opts)
+{
+ /* note: params[0] will be 'remove' */
+ if (!opts->params[0] || !opts->params[1])
+ return Err(Error::Code::InvalidArgument,
+ "expected some files to remove");
+
+ for (auto u = 1; opts->params[u]; ++u) {
+
+ const auto res = store.remove_message(opts->params[u]);
+ if (!res)
+ return Err(Error::Code::File, "failed to remove %s",
+ opts->params[u]);
+ else
+ g_debug("removed message @ %s", opts->params[u]);
+ }
+
+ return Ok();
+}
+
+
+template <typename T>
+static void
+key_val(const Mu::MaybeAnsi& col, const std::string& key, T val)
+{
+ using Color = Mu::MaybeAnsi::Color;
+
+ std::cout << col.fg(Color::BrightBlue) << std::left << std::setw(18) << key << col.reset()
+ << ": ";
+
+ std::cout << col.fg(Color::Green) << val << col.reset() << "\n";
+}
+
+
+static void
+print_signature(const Mu::MimeSignature& sig, const MuConfig *opts)
+{
+ Mu::MaybeAnsi col{!opts->nocolor};
+
+ const auto created{sig.created()};
+ key_val(col, "created",
+ created == 0 ? "unknown" :
+ time_to_string("%c", sig.created()).c_str());
+
+ const auto expires{sig.expires()};
+ key_val(col, "expires", expires==0 ? "never" :
+ time_to_string("%c", sig.expires()).c_str());
+
+ const auto cert{sig.certificate()};
+ key_val(col, "public-key algo",
+ to_string_view_opt(cert.pubkey_algo()).value_or("unknown"));
+ key_val(col, "digest algo",
+ to_string_view_opt(cert.digest_algo()).value_or("unknown"));
+ key_val(col, "id-validity",
+ to_string_view_opt(cert.id_validity()).value_or("unknown"));
+ key_val(col, "trust",
+ to_string_view_opt(cert.trust()).value_or("unknown"));
+ key_val(col, "issuer-serial", cert.issuer_serial().value_or("unknown"));
+ key_val(col, "issuer-name", cert.issuer_name().value_or("unknown"));
+ key_val(col, "finger-print", cert.fingerprint().value_or("unknown"));
+ key_val(col, "key-id", cert.key_id().value_or("unknown"));
+ key_val(col, "name", cert.name().value_or("unknown"));
+ key_val(col, "user-id", cert.user_id().value_or("unknown"));
+}
+
+
+static bool
+verify(const MimeMultipartSigned& sigpart, const MuConfig *opts)
+{
+ using VFlags = MimeMultipartSigned::VerifyFlags;
+ const auto vflags{opts->auto_retrieve ?
+ VFlags::EnableKeyserverLookups: VFlags::None};
+
+ auto ctx{MimeCryptoContext::make_gpg()};
+ if (!ctx)
+ return false;
+
+ const auto sigs{sigpart.verify(*ctx, vflags)};
+ Mu::MaybeAnsi col{!opts->nocolor};
+
+ if (!sigs || sigs->empty()) {
+
+ if (!opts->quiet)
+ g_print("cannot find signatures in part\n");
+
+ return true;
+ }
+
+ bool valid{true};
+ for (auto&& sig: *sigs) {
+
+ const auto status{sig.status()};
+
+ if (!opts->quiet)
+ key_val(col, "status", to_string(status));
+
+ if (opts->verbose)
+ print_signature(sig, opts);
+
+ if (none_of(sig.status() & MimeSignature::Status::Green))
+ valid = false;
+ }
+
+ return valid;
+}
+
+static Mu::Result<void>
+cmd_verify(const MuConfig* opts)
+{
+ if (!opts || opts->cmd != MU_CONFIG_CMD_VERIFY)
+ return Err(Error::Code::Internal, "error in parameters");
+
+ if (!opts->params[1])
+ return Err(Error::Code::InvalidArgument,
+ "missing message-file parameter");
+
+ auto message{Message::make_from_path(opts->params[1],
+ mu_config_message_options(opts))};
+ if (!message)
+ return Err(message.error());
+
+
+ if (none_of(message->flags() & Flags::Signed)) {
+ if (!opts->quiet)
+ g_print("no signed parts found\n");
+ return Ok();
+ }
+
+ bool verified{true}; /* innocent until proven guilty */
+ for(auto&& part: message->parts()) {
+
+ if (!part.is_signed())
+ continue;
+
+ const auto& mobj{part.mime_object()};
+ if (!mobj.is_multipart_signed())
+ continue;
+
+ if (!verify(MimeMultipartSigned(mobj), opts))
+ verified = false;
+ }
+
+ if (verified)
+ return Ok();
+ else
+ return Err(Error::Code::UnverifiedSignature,
+ "failed to verify one or more signatures");
+}
+
+static Result<void>
+cmd_info(const Mu::Store& store, const MuConfig* opts)
+{
+ using namespace tabulate;
+
+ if (!locale_workaround())
+ return Err(Error::Code::User, "failed to find a working locale");
+
+ auto colorify = [](Table& table) {
+ for (auto&& row: table) {
+
+ if (row.cells().size() < 2)
+ continue;
+
+ row.cells().at(0)->format().font_style({FontStyle::bold})
+ .font_color(Color::green);
+ row.cells().at(1)->format().font_color(Color::blue);
+ }
+ };
+
+ auto tstamp = [](::time_t t)->std::string {
+ if (t == 0)
+ return "never";
+ else
+ return time_to_string("%c", t);
+
+ };
+
+ Table info;
+ info.add_row({"maildir", store.properties().root_maildir});
+ info.add_row({"database-path", store.properties().database_path});
+ info.add_row({"schema-version", store.properties().schema_version});
+ info.add_row({"max-message-size", format("%zu", store.properties().max_message_size)});
+ info.add_row({"batch-size", format("%zu", store.properties().batch_size)});
+ info.add_row({"created", tstamp(store.properties().created)});
+ for (auto&& c : store.properties().personal_addresses)
+ info.add_row({"personal-address", c});
+
+ info.add_row({"messages in store", format("%zu", store.size())});
+ info.add_row({"last-change", tstamp(store.statistics().last_change)});
+ info.add_row({"last-index", tstamp(store.statistics().last_index)});
+
+ if (!opts->nocolor)
+ colorify(info);
+
+ std::cout << info << '\n';
+
+ return Ok();
+}
+
+static Result<void>
+cmd_init(const MuConfig* opts)
+{
+ /* not provided, nor could we find a good default */
+ if (!opts->maildir)
+ return Err(Error::Code::InvalidArgument,
+ "missing --maildir parameter and could "
+ "not determine default");
+
+ if (opts->max_msg_size < 0)
+ return Err(Error::Code::InvalidArgument,
+ "invalid value for max-message-size");
+ else if (opts->batch_size < 0)
+ return Err(Error::Code::InvalidArgument,
+ "invalid value for batch-size");
+
+ Mu::Store::Config conf{};
+ conf.max_message_size = opts->max_msg_size;
+ conf.batch_size = opts->batch_size;
+
+ Mu::StringVec my_addrs;
+ auto addrs = opts->my_addresses;
+ while (addrs && *addrs) {
+ my_addrs.emplace_back(*addrs);
+ ++addrs;
+ }
+
+ auto store = Store::make_new(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB),
+ opts->maildir, my_addrs, conf);
+ if (!store)
+ return Err(store.error());
+
+ if (!opts->quiet) {
+ cmd_info(*store, opts);
+ std::cout << "\nstore created; use the 'index' command to fill/update it.\n";
+ }
+
+ return Ok();
+}
+
+static Result<void>
+cmd_find(const MuConfig* opts)
+{
+ auto store{Store::make(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB))};
+ if (!store)
+ return Err(store.error());
+ else
+ return mu_cmd_find(*store, opts);
+}
+
+static void
+show_usage(void)
+{
+ g_print("usage: mu command [options] [parameters]\n");
+ g_print("where command is one of index, find, cfind, view, mkdir, "
+ "extract, add, remove, script, verify or server\n");
+ g_print("see the mu, mu-<command> or mu-easy manpages for "
+ "more information\n");
+}
+
+
+using ReadOnlyStoreFunc = std::function<Result<void>(const Store&, const MuConfig*)>;
+using WritableStoreFunc = std::function<Result<void>(Store&, const MuConfig*)>;
+
+static Result<void>
+with_readonly_store(const ReadOnlyStoreFunc& func, const MuConfig* opts)
+{
+ auto store{Store::make(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB))};
+ if (!store)
+ return Err(store.error());
+
+ return func(store.value(), opts);
+}
+
+static Result<void>
+with_writable_store(const WritableStoreFunc func, const MuConfig* opts)
+{
+ auto store{Store::make(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB),
+ Store::Options::Writable)};
+ if (!store)
+ return Err(store.error());
+
+ return func(store.value(), opts);
+}
+
+Result<void>
+Mu::mu_cmd_execute(const MuConfig* opts) try {
+
+ if (!opts || !opts->params || !opts->params[0])
+ return Err(Error::Code::InvalidArgument, "error in parameters");
+
+ switch (opts->cmd) {
+ case MU_CONFIG_CMD_HELP: /* already handled in mu-config.c */
+ return Ok();
+
+ /*
+ * no store needed
+ */
+ case MU_CONFIG_CMD_FIELDS:
+ return mu_cmd_fields(opts);
+ case MU_CONFIG_CMD_MKDIR:
+ return cmd_mkdir(opts);
+ case MU_CONFIG_CMD_SCRIPT:
+ return mu_cmd_script(opts);
+ case MU_CONFIG_CMD_VIEW:
+ return cmd_view(opts);
+ case MU_CONFIG_CMD_VERIFY:
+ return cmd_verify(opts);
+ case MU_CONFIG_CMD_EXTRACT:
+ return mu_cmd_extract(opts);
+ /*
+ * read-only store
+ */
+
+ case MU_CONFIG_CMD_CFIND:
+ return with_readonly_store(mu_cmd_cfind, opts);
+ case MU_CONFIG_CMD_FIND:
+ return cmd_find(opts);
+ case MU_CONFIG_CMD_INFO:
+ return with_readonly_store(cmd_info, opts);
+
+ /* writable store */
+
+ case MU_CONFIG_CMD_ADD:
+ return with_writable_store(cmd_add, opts);
+ case MU_CONFIG_CMD_REMOVE:
+ return with_writable_store(cmd_remove, opts);
+ case MU_CONFIG_CMD_INDEX:
+ return with_writable_store(mu_cmd_index, opts);
+
+ /* commands instantiate store themselves */
+ case MU_CONFIG_CMD_INIT:
+ return cmd_init(opts);
+ case MU_CONFIG_CMD_SERVER:
+ return mu_cmd_server(opts);
+
+ default:
+ show_usage();
+ return Ok();
+ }
+
+} catch (const Mu::Error& er) {
+ return Err(er);
+} catch (const std::runtime_error& re) {
+ return Err(Error::Code::Internal, "runtime-error: %s", re.what());
+} catch (const std::exception& ex) {
+ return Err(Error::Code::Internal, "error: %s", ex.what());
+} catch (...) {
+ return Err(Error::Code::Internal, "caught exception");
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2022-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_CMD_HH__
+#define MU_CMD_HH__
+
+#include <glib.h>
+#include <mu-config.hh>
+#include <mu-store.hh>
+#include <utils/mu-result.hh>
+
+namespace Mu {
+/**
+ * execute the 'find' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_find(const Mu::Store& store, const MuConfig* opts);
+
+/**
+ * execute the 'extract' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_extract(const MuConfig* opts);
+
+/**
+ * execute the 'fields' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_fields(const MuConfig* opts);
+
+/**
+ * execute the 'script' command
+ *
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_script(const MuConfig* opts);
+
+/**
+ * execute the cfind command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_cfind(const Mu::Store& store, const MuConfig* opts);
+
+/**
+ * execute the 'index' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_index(Mu::Store& store, const MuConfig* opt);
+
+/**
+ * execute the server command
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return MU_OK (0) if the command succeeds, some error code otherwise
+ */
+Result<void> mu_cmd_server(const MuConfig* opts);
+
+/**
+ * execute some mu command, based on 'opts'
+ *
+ * @param opts configuration option
+ * @param err receives error information, or NULL
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_execute(const MuConfig* opts);
+
+} // namespace Mu
+
+#endif /*__MU_CMD_H__*/
--- /dev/null
+/*
+** 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 <string.h> /* memset */
+#include <unistd.h>
+#include <stdio.h>
+
+#include "mu-config.hh"
+#include "mu-cmd.hh"
+
+using namespace Mu;
+
+static MuConfig MU_CONFIG;
+
+#define color_maybe(C) (MU_CONFIG.nocolor ? "" : (C))
+
+static MuConfigFormat
+get_output_format(const char* formatstr)
+{
+ int i;
+ struct {
+ const char* name;
+ MuConfigFormat format;
+ } formats[] = {{"mutt-alias", MU_CONFIG_FORMAT_MUTT_ALIAS},
+ {"mutt-ab", MU_CONFIG_FORMAT_MUTT_AB},
+ {"wl", MU_CONFIG_FORMAT_WL},
+ {"csv", MU_CONFIG_FORMAT_CSV},
+ {"org-contact", MU_CONFIG_FORMAT_ORG_CONTACT},
+ {"bbdb", MU_CONFIG_FORMAT_BBDB},
+ {"links", MU_CONFIG_FORMAT_LINKS},
+ {"plain", MU_CONFIG_FORMAT_PLAIN},
+ {"sexp", MU_CONFIG_FORMAT_SEXP},
+ {"json", MU_CONFIG_FORMAT_JSON},
+ {"xml", MU_CONFIG_FORMAT_XML},
+ {"xquery", MU_CONFIG_FORMAT_XQUERY},
+ {"mquery", MU_CONFIG_FORMAT_MQUERY},
+ {"debug", MU_CONFIG_FORMAT_DEBUG}};
+
+ for (i = 0; i != G_N_ELEMENTS(formats); i++)
+ if (strcmp(formats[i].name, formatstr) == 0)
+ return formats[i].format;
+
+ return MU_CONFIG_FORMAT_UNKNOWN;
+}
+
+#define expand_dir(D) \
+ if ((D)) { \
+ char* exp; \
+ exp = mu_util_dir_expand((D)); \
+ if (exp) { \
+ g_free((D)); \
+ (D) = exp; \
+ } \
+ }
+
+static void
+set_group_mu_defaults()
+{
+ /* try to determine muhome from command-line or environment;
+ * note: if not specified, we use XDG defaults */
+
+ if (!MU_CONFIG.muhome) {
+ /* if not set explicity, try the environment */
+ const char* muhome;
+ muhome = g_getenv("MUHOME");
+ if (muhome)
+ MU_CONFIG.muhome = g_strdup(muhome);
+ }
+
+ if (MU_CONFIG.muhome)
+ expand_dir(MU_CONFIG.muhome);
+
+ /* check for the MU_NOCOLOR or NO_COLOR env vars; but in any case don't
+ * use colors unless we're writing to a tty */
+ if (g_getenv(MU_NOCOLOR) != NULL || g_getenv("NO_COLOR") != NULL)
+ MU_CONFIG.nocolor = TRUE;
+
+ if (!isatty(fileno(stdout)) || !isatty(fileno(stderr)))
+ MU_CONFIG.nocolor = TRUE;
+}
+
+static GOptionGroup*
+config_options_group_mu()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {
+ {"debug", 'd', 0, G_OPTION_ARG_NONE, &MU_CONFIG.debug,
+ "print debug output to standard error (false)", NULL},
+ {"quiet", 'q', 0, G_OPTION_ARG_NONE, &MU_CONFIG.quiet,
+ "don't give any progress information (false)", NULL},
+ {"version", 'V', 0, G_OPTION_ARG_NONE, &MU_CONFIG.version,
+ "display version and copyright information (false)", NULL},
+ {"muhome", 0, 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.muhome,
+ "specify an alternative mu directory", "<dir>"},
+ {"log-stderr", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.log_stderr,
+ "log to standard error (false)", NULL},
+ {"nocolor", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.nocolor,
+ "don't use ANSI-colors in output (false)", NULL},
+ {"verbose", 'v', 0, G_OPTION_ARG_NONE, &MU_CONFIG.verbose,
+ "verbose output (false)", NULL},
+
+ {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &MU_CONFIG.params,
+ "parameters", NULL},
+ {NULL, 0, 0, (GOptionArg)0, NULL, NULL, NULL}};
+
+ og = g_option_group_new("mu", "general mu options", "", NULL, NULL);
+ g_option_group_add_entries(og, entries);
+
+ return og;
+}
+
+static void
+set_group_init_defaults()
+{
+ if (!MU_CONFIG.maildir)
+ MU_CONFIG.maildir = mu_util_guess_maildir();
+
+ expand_dir(MU_CONFIG.maildir);
+}
+
+static GOptionGroup*
+config_options_group_init()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {
+ {"maildir", 'm', 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.maildir,
+ "top of the maildir", "<maildir>"},
+ {"my-address", 0, 0, G_OPTION_ARG_STRING_ARRAY, &MU_CONFIG.my_addresses,
+ "my e-mail address; can be used multiple times", "<address>"},
+ {"max-message-size", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.max_msg_size,
+ "Maximum allowed size for messages", "<size-in-bytes>"},
+ {"batch-size", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.batch_size,
+ "Number of changes in a database transaction batch", "<number>"},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ og = g_option_group_new("init", "Options for the 'init' command", "", NULL, NULL);
+ g_option_group_add_entries(og, entries);
+
+ return og;
+}
+
+static gboolean
+index_post_parse_func(GOptionContext* context, GOptionGroup* group, gpointer data,
+ GError** error)
+{
+ if (!MU_CONFIG.maildir && !MU_CONFIG.my_addresses)
+ return TRUE;
+
+ g_printerr("%sNOTE%s: as of mu 1.3.8, 'mu index' no longer uses the\n"
+ "--maildir/-m or --my-address options.\n\n",
+ color_maybe(MU_COLOR_RED), color_maybe(MU_COLOR_DEFAULT));
+ g_printerr("Instead, these options should be passed to 'mu init'.\n");
+ g_printerr(
+ "See the mu-init(1) or the mu4e reference manual,\n'Initializing the message "
+ "store' for details.\n\n");
+
+ return TRUE;
+}
+
+static GOptionGroup*
+config_options_group_index()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {
+ /* only here so we can tell users they are deprecated */
+ {"maildir", 'm', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_FILENAME,
+ &MU_CONFIG.maildir, "top of the maildir", "<maildir>"},
+ {"my-address", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING_ARRAY,
+ &MU_CONFIG.my_addresses, "my e-mail address; can be used multiple times",
+ "<address>"},
+
+ {"lazy-check", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.lazycheck,
+ "only check dir-timestamps (false)", NULL},
+ {"nocleanup", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.nocleanup,
+ "don't clean up the database after indexing (false)", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ og = g_option_group_new("index", "Options for the 'index' command", "", NULL,
+ NULL);
+ g_option_group_add_entries(og, entries);
+ g_option_group_set_parse_hooks(og, NULL, (GOptionParseFunc)index_post_parse_func);
+
+ return og;
+}
+
+static void
+set_group_find_defaults()
+{
+ /* note, when no fields are specified, we use date-from-subject */
+ if (!MU_CONFIG.fields || !*MU_CONFIG.fields) {
+ MU_CONFIG.fields = g_strdup("d f s");
+ if (!MU_CONFIG.sortfield) {
+ MU_CONFIG.sortfield = g_strdup("d");
+ }
+ }
+
+ if (!MU_CONFIG.formatstr) /* by default, use plain output */
+ MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN;
+ else
+ MU_CONFIG.format = get_output_format(MU_CONFIG.formatstr);
+
+ expand_dir(MU_CONFIG.linksdir);
+}
+
+static GOptionGroup*
+config_options_group_find()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {
+ {"fields", 'f', 0, G_OPTION_ARG_STRING, &MU_CONFIG.fields,
+ "fields to display in the output", "<fields>"},
+ {"sortfield", 's', 0, G_OPTION_ARG_STRING, &MU_CONFIG.sortfield,
+ "field to sort on", "<field>"},
+ {"maxnum", 'n', 0, G_OPTION_ARG_INT, &MU_CONFIG.maxnum,
+ "number of entries to display in the output", "<number>"},
+ {"threads", 't', 0, G_OPTION_ARG_NONE, &MU_CONFIG.threads,
+ "show message threads", NULL},
+ {"bookmark", 'b', 0, G_OPTION_ARG_STRING, &MU_CONFIG.bookmark,
+ "use a bookmarked query", "<bookmark>"},
+ {"reverse", 'z', 0, G_OPTION_ARG_NONE, &MU_CONFIG.reverse,
+ "sort in reverse (descending) order (z -> a)", NULL},
+ {"skip-dups", 'u', 0, G_OPTION_ARG_NONE, &MU_CONFIG.skip_dups,
+ "show only the first of messages duplicates (false)", NULL},
+ {"include-related", 'r', 0, G_OPTION_ARG_NONE, &MU_CONFIG.include_related,
+ "include related messages in results (false)", NULL},
+ {"linksdir", 0, 0, G_OPTION_ARG_STRING, &MU_CONFIG.linksdir,
+ "output as symbolic links to a target maildir", "<dir>"},
+ {"clearlinks", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.clearlinks,
+ "clear old links before filling a linksdir (false)", NULL},
+ {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr,
+ "output format ('plain'(*), 'links', 'xml',"
+ "'sexp', 'xquery')",
+ "<format>"},
+ {"summary-len", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.summary_len,
+ "use up to <n> lines for the summary, or 0 for none (0)", "<len>"},
+ {"exec", 'e', 0, G_OPTION_ARG_STRING, &MU_CONFIG.exec,
+ "execute command on each match message", "<command>"},
+ {"after", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.after,
+ "only show messages whose m_time > T (t_time)", "<timestamp>"},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ og = g_option_group_new("find", "Options for the 'find' command", "", NULL, NULL);
+ g_option_group_add_entries(og, entries);
+
+ return og;
+}
+
+static GOptionGroup*
+config_options_group_mkdir()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {{"mode", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.dirmode,
+ "set the mode (as in chmod), in octal notation",
+ "<mode>"},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ /* set dirmode before, because '0000' is a valid mode */
+ MU_CONFIG.dirmode = 0755;
+
+ og = g_option_group_new("mkdir", "Options for the 'mkdir' command", "", NULL,
+ NULL);
+ g_option_group_add_entries(og, entries);
+
+ return og;
+}
+
+static void
+set_group_cfind_defaults()
+{
+ if (!MU_CONFIG.formatstr) /* by default, use plain output */
+ MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN;
+ else
+ MU_CONFIG.format = get_output_format(MU_CONFIG.formatstr);
+}
+
+static GOptionGroup*
+config_options_group_cfind()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {
+ {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr,
+ "output format (plain(*), mutt-alias, mutt-ab, wl, "
+ "org-contact, bbdb, csv)",
+ "<format>"},
+ {"personal", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.personal,
+ "whether to only get 'personal' contacts", NULL},
+ {"after", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.after,
+ "only get addresses last seen after T", "<timestamp>"},
+ {"maxnum", 'n', 0, G_OPTION_ARG_INT, &MU_CONFIG.maxnum,
+ "maximum number of contacts", "<number>"},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ og = g_option_group_new("cfind", "Options for the 'cfind' command", "", NULL,
+ NULL);
+ g_option_group_add_entries(og, entries);
+
+ return og;
+}
+
+static GOptionGroup*
+config_options_group_script()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY,
+ &MU_CONFIG.params, "script parameters", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ og = g_option_group_new("script", "Options for the 'script' command", "", NULL,
+ NULL);
+
+ g_option_group_add_entries(og, entries);
+
+ return og;
+}
+
+static void
+set_group_view_defaults()
+{
+ if (!MU_CONFIG.formatstr) /* by default, use plain output */
+ MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN;
+ else
+ MU_CONFIG.format = get_output_format(MU_CONFIG.formatstr);
+}
+
+/* crypto options are used in a few different commands */
+static GOptionEntry*
+crypto_option_entries()
+{
+ static GOptionEntry entries[] = {
+ {"auto-retrieve", 'r', 0, G_OPTION_ARG_NONE, &MU_CONFIG.auto_retrieve,
+ "attempt to retrieve keys online (false)", NULL},
+ {"decrypt", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.decrypt,
+ "attempt to decrypt the message", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ return entries;
+}
+
+static GOptionGroup*
+config_options_group_view()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {
+ {"summary-len", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.summary_len,
+ "use up to <n> lines for the summary, or 0 for none (0)", "<len>"},
+ {"terminate", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.terminator,
+ "terminate messages with ascii-0x07 (\\f, form-feed)", NULL},
+ {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr,
+ "output format ('plain'(*), 'sexp')", "<format>"},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ og = g_option_group_new("view", "Options for the 'view' command", "", NULL, NULL);
+
+ g_option_group_add_entries(og, entries);
+ g_option_group_add_entries(og, crypto_option_entries());
+
+ return og;
+}
+
+static void
+set_group_extract_defaults()
+{
+ if (!MU_CONFIG.targetdir)
+ MU_CONFIG.targetdir = g_strdup(".");
+
+ expand_dir(MU_CONFIG.targetdir);
+}
+
+static GOptionGroup*
+config_options_group_extract()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {
+ {"save-attachments", 'a', 0, G_OPTION_ARG_NONE, &MU_CONFIG.save_attachments,
+ "save all attachments (false)", NULL},
+ {"save-all", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.save_all,
+ "save all parts (incl. non-attachments) (false)", NULL},
+ {"parts", 0, 0, G_OPTION_ARG_STRING, &MU_CONFIG.parts,
+ "save specific parts (comma-separated list)", "<parts>"},
+ {"target-dir", 0, 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.targetdir,
+ "target directory for saving", "<dir>"},
+ {"overwrite", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.overwrite,
+ "overwrite existing files (false)", NULL},
+ {"play", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.play,
+ "try to 'play' (open) the extracted parts", NULL},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+ og = g_option_group_new("extract", "Options for the 'extract' command", "", NULL,
+ NULL);
+ g_option_group_add_entries(og, entries);
+ g_option_group_add_entries(og, crypto_option_entries());
+
+ return og;
+}
+
+static GOptionGroup*
+config_options_group_verify()
+{
+ GOptionGroup* og;
+ og = g_option_group_new("verify", "Options for the 'verify' command", "", NULL,
+ NULL);
+ g_option_group_add_entries(og, crypto_option_entries());
+
+ return og;
+}
+
+static GOptionGroup*
+config_options_group_server()
+{
+ GOptionGroup* og;
+ GOptionEntry entries[] = {
+ {"commands", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.commands,
+ "list the available command and their parameters, then exit", NULL},
+ {"eval", 'e', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &MU_CONFIG.eval,
+ "expression to evaluate", "<expr>"},
+ {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+ og = g_option_group_new("server", "Options for the 'server' command", "", NULL,
+ NULL);
+ g_option_group_add_entries(og, entries);
+
+ return og;
+}
+
+static MuConfigCmd
+cmd_from_string(const char* str)
+{
+ int i;
+ struct {
+ const gchar* name;
+ MuConfigCmd cmd;
+ } cmd_map[] = {
+ {"add", MU_CONFIG_CMD_ADD}, {"cfind", MU_CONFIG_CMD_CFIND},
+ {"extract", MU_CONFIG_CMD_EXTRACT}, {"find", MU_CONFIG_CMD_FIND},
+ {"help", MU_CONFIG_CMD_HELP}, {"index", MU_CONFIG_CMD_INDEX},
+ {"info", MU_CONFIG_CMD_INFO}, {"init", MU_CONFIG_CMD_INIT},
+ {"mkdir", MU_CONFIG_CMD_MKDIR}, {"remove", MU_CONFIG_CMD_REMOVE},
+ {"script", MU_CONFIG_CMD_SCRIPT}, {"server", MU_CONFIG_CMD_SERVER},
+ {"verify", MU_CONFIG_CMD_VERIFY}, {"view", MU_CONFIG_CMD_VIEW},
+ {"fields", MU_CONFIG_CMD_FIELDS},
+ };
+
+ if (!str)
+ return MU_CONFIG_CMD_UNKNOWN;
+
+ for (i = 0; i != G_N_ELEMENTS(cmd_map); ++i)
+ if (strcmp(str, cmd_map[i].name) == 0)
+ return cmd_map[i].cmd;
+#ifdef BUILD_GUILE
+ /* if we don't recognize it and it's not an option, it may be
+ * some script */
+ if (str[0] != '-')
+ return MU_CONFIG_CMD_SCRIPT;
+#endif /*BUILD_GUILE*/
+
+ return MU_CONFIG_CMD_UNKNOWN;
+}
+
+static gboolean
+parse_cmd(int* argcp, char*** argvp, GError** err)
+{
+ MU_CONFIG.cmd = MU_CONFIG_CMD_NONE;
+ MU_CONFIG.cmdstr = NULL;
+
+ if (*argcp < 2) /* no command found at all */
+ return TRUE;
+ else if ((**argvp)[1] == '-')
+ /* if the first param starts with '-', there is no
+ * command, just some option (like --version, --help
+ * etc.)*/
+ return TRUE;
+
+ MU_CONFIG.cmdstr = g_strdup((*argvp)[1]);
+ MU_CONFIG.cmd = cmd_from_string(MU_CONFIG.cmdstr);
+
+#ifndef BUILD_GUILE
+ if (MU_CONFIG.cmd == MU_CONFIG_CMD_SCRIPT) {
+ mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS,
+ "command 'script' not supported");
+ return FALSE;
+ }
+#endif /*!BUILD_GUILE*/
+
+ if (MU_CONFIG.cmdstr && MU_CONFIG.cmdstr[0] != '-' &&
+ MU_CONFIG.cmd == MU_CONFIG_CMD_UNKNOWN) {
+ mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS, "unknown command '%s'",
+ MU_CONFIG.cmdstr);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static GOptionGroup*
+get_option_group(MuConfigCmd cmd)
+{
+ switch (cmd) {
+ case MU_CONFIG_CMD_CFIND: return config_options_group_cfind();
+ case MU_CONFIG_CMD_EXTRACT: return config_options_group_extract();
+ case MU_CONFIG_CMD_FIND: return config_options_group_find();
+ case MU_CONFIG_CMD_INDEX: return config_options_group_index();
+ case MU_CONFIG_CMD_INIT: return config_options_group_init();
+ case MU_CONFIG_CMD_MKDIR: return config_options_group_mkdir();
+ case MU_CONFIG_CMD_SERVER: return config_options_group_server();
+ case MU_CONFIG_CMD_SCRIPT: return config_options_group_script();
+ case MU_CONFIG_CMD_VERIFY: return config_options_group_verify();
+ case MU_CONFIG_CMD_VIEW: return config_options_group_view();
+ default: return NULL; /* no group to add */
+ }
+}
+
+/* ugh yuck massaging the GOption text output; glib prepares some text
+ * which has a 'Usage:' for the 'help' command. However, we need the
+ * help for the command we're asking help for. So, we remove the Usage:
+ * from what glib generates. :-( */
+static gchar*
+massage_help(const char* help)
+{
+ GRegex* rx;
+ char* str;
+
+ rx = g_regex_new("^Usage:.*\n.*\n", (GRegexCompileFlags)0,
+ G_REGEX_MATCH_NEWLINE_ANY, NULL);
+ str = g_regex_replace(rx, help, -1, 0, "", G_REGEX_MATCH_NEWLINE_ANY, NULL);
+ g_regex_unref(rx);
+ return str;
+}
+
+static const char*
+get_help_string(MuConfigCmd cmd, bool long_help)
+{
+ struct Help {
+ MuConfigCmd cmd;
+ const char* usage;
+ const char* long_help;
+ };
+ constexpr std::array all_help = {
+#include "mu-help-strings.inc"
+ };
+
+ const auto help_it = std::find_if(all_help.begin(), all_help.end(),
+ [&](auto&& info) { return info.cmd == cmd; });
+ if (help_it == all_help.end()) {
+ g_critical("cannot find info for %u", cmd);
+ return "";
+ } else
+ return long_help ? help_it->long_help : help_it->usage;
+}
+
+void
+Mu::mu_config_show_help(MuConfigCmd cmd)
+{
+ GOptionContext* ctx;
+ GOptionGroup* group;
+ char * help, *cleanhelp;
+
+ g_return_if_fail(mu_config_cmd_is_valid(cmd));
+
+ ctx = g_option_context_new("- mu help");
+ g_option_context_set_main_group(ctx, config_options_group_mu());
+
+ group = get_option_group(cmd);
+ if (group)
+ g_option_context_add_group(ctx, group);
+
+ g_option_context_set_description(ctx, get_help_string(cmd, TRUE));
+ help = g_option_context_get_help(ctx, TRUE, group);
+ cleanhelp = massage_help(help);
+
+ g_print("usage:\n\t%s%s", get_help_string(cmd, FALSE), cleanhelp);
+
+ g_free(help);
+ g_free(cleanhelp);
+ g_option_context_free(ctx);
+}
+
+static gboolean
+cmd_help()
+{
+ MuConfigCmd cmd;
+
+ if (!MU_CONFIG.params)
+ cmd = MU_CONFIG_CMD_UNKNOWN;
+ else
+ cmd = cmd_from_string(MU_CONFIG.params[1]);
+
+ if (cmd == MU_CONFIG_CMD_UNKNOWN) {
+ mu_config_show_help(MU_CONFIG_CMD_HELP);
+ return TRUE;
+ }
+
+ mu_config_show_help(cmd);
+
+ return TRUE;
+}
+
+static gboolean
+parse_params(int* argcp, char*** argvp, GError** err)
+{
+ GOptionContext* context;
+ GOptionGroup* group;
+ gboolean rv;
+
+ context = g_option_context_new("- mu general options");
+
+ g_option_context_set_help_enabled(context, FALSE);
+ g_option_context_set_main_group(context, config_options_group_mu());
+ g_option_context_set_ignore_unknown_options(context, FALSE);
+
+ switch (MU_CONFIG.cmd) {
+ case MU_CONFIG_CMD_NONE:
+ case MU_CONFIG_CMD_HELP:
+ /* 'help' is special; sucks in the options of the
+ * command after it */
+ rv = g_option_context_parse(context, argcp, argvp, err) && cmd_help();
+ break;
+ case MU_CONFIG_CMD_SCRIPT:
+ /* all unknown commands are passed to 'script' */
+ g_option_context_set_ignore_unknown_options(context, TRUE);
+ group = get_option_group(MU_CONFIG.cmd);
+ g_option_context_add_group(context, group);
+ rv = g_option_context_parse(context, argcp, argvp, err);
+ MU_CONFIG.script = g_strdup(MU_CONFIG.cmdstr);
+ /* argvp contains the script parameters */
+ MU_CONFIG.script_params = (const char**)&((*argvp)[1]);
+ break;
+
+ default:
+ group = get_option_group(MU_CONFIG.cmd);
+ if (group)
+ g_option_context_add_group(context, group);
+
+ rv = g_option_context_parse(context, argcp, argvp, err);
+ break;
+ }
+
+ g_option_context_free(context);
+
+ return rv ? TRUE : FALSE;
+}
+
+MuConfig*
+Mu::mu_config_init(int* argcp, char*** argvp, GError** err)
+{
+ g_return_val_if_fail(argcp && argvp, NULL);
+
+ memset(&MU_CONFIG, 0, sizeof(MU_CONFIG));
+
+ if (!parse_cmd(argcp, argvp, err))
+ goto errexit;
+
+ if (!parse_params(argcp, argvp, err))
+ goto errexit;
+
+ /* fill in the defaults if user did not specify */
+ set_group_mu_defaults();
+ set_group_init_defaults();
+ set_group_find_defaults();
+ set_group_cfind_defaults();
+ set_group_view_defaults();
+ set_group_extract_defaults();
+ /* set_group_mkdir_defaults (config); */
+
+ return &MU_CONFIG;
+
+errexit:
+ mu_config_uninit(&MU_CONFIG);
+ return NULL;
+}
+
+void
+Mu::mu_config_uninit(MuConfig* opts)
+{
+ if (!opts)
+ return;
+
+ g_free(opts->cmdstr);
+ g_free(opts->muhome);
+ g_free(opts->maildir);
+ g_free(opts->fields);
+ g_free(opts->sortfield);
+ g_free(opts->bookmark);
+ g_free(opts->formatstr);
+ g_free(opts->exec);
+ g_free(opts->linksdir);
+ g_free(opts->targetdir);
+ g_free(opts->parts);
+ g_free(opts->script);
+ g_free(opts->eval);
+
+ g_strfreev(opts->my_addresses);
+ g_strfreev(opts->params);
+
+ memset(opts, 0, sizeof(MU_CONFIG));
+}
+
+size_t
+Mu::mu_config_param_num(const MuConfig* opts)
+{
+ size_t n;
+
+ g_return_val_if_fail(opts && opts->params, 0);
+ for (n = 0; opts->params[n]; ++n)
+ ;
+
+ return n;
+}
+
+Message::Options
+Mu::mu_config_message_options(const MuConfig *conf)
+{
+ Message::Options opts{};
+
+ if (conf->decrypt)
+ opts |= Message::Options::Decrypt;
+ if (conf->auto_retrieve)
+ opts |= Message::Options::RetrieveKeys;
+
+ return opts;
+}
--- /dev/null
+/*
+** 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_CONFIG_HH__
+#define MU_CONFIG_HH__
+
+#include <glib.h>
+#include <sys/types.h> /* for mode_t */
+#include <message/mu-message.hh>
+#include <utils/mu-util.h>
+
+namespace Mu {
+
+/* env var; if non-empty, color are disabled */
+#define MU_NOCOLOR "MU_NOCOLOR"
+
+typedef enum {
+ MU_CONFIG_FORMAT_UNKNOWN = 0,
+
+ /* for cfind, find, view */
+ MU_CONFIG_FORMAT_PLAIN, /* plain output */
+
+ /* for cfind */
+ MU_CONFIG_FORMAT_MUTT_ALIAS, /* mutt alias style */
+ MU_CONFIG_FORMAT_MUTT_AB, /* mutt ext abook */
+ MU_CONFIG_FORMAT_WL, /* Wanderlust abook */
+ MU_CONFIG_FORMAT_CSV, /* comma-sep'd values */
+ MU_CONFIG_FORMAT_ORG_CONTACT, /* org-contact */
+ MU_CONFIG_FORMAT_BBDB, /* BBDB */
+ MU_CONFIG_FORMAT_DEBUG,
+
+ /* for find, view */
+ MU_CONFIG_FORMAT_SEXP, /* output sexps (emacs) */
+ MU_CONFIG_FORMAT_JSON, /* output JSON */
+
+ /* for find */
+ MU_CONFIG_FORMAT_LINKS, /* output as symlinks */
+ MU_CONFIG_FORMAT_XML, /* output xml */
+ MU_CONFIG_FORMAT_XQUERY, /* output the xapian query */
+ MU_CONFIG_FORMAT_MQUERY, /* output the mux query */
+
+ MU_CONFIG_FORMAT_EXEC /* execute some command */
+} MuConfigFormat;
+
+typedef enum {
+ MU_CONFIG_CMD_UNKNOWN = 0,
+
+ MU_CONFIG_CMD_ADD,
+ MU_CONFIG_CMD_CFIND,
+ MU_CONFIG_CMD_EXTRACT,
+ MU_CONFIG_CMD_FIELDS,
+ MU_CONFIG_CMD_FIND,
+ MU_CONFIG_CMD_HELP,
+ MU_CONFIG_CMD_INDEX,
+ MU_CONFIG_CMD_INFO,
+ MU_CONFIG_CMD_INIT,
+ MU_CONFIG_CMD_MKDIR,
+ MU_CONFIG_CMD_REMOVE,
+ MU_CONFIG_CMD_SCRIPT,
+ MU_CONFIG_CMD_SERVER,
+ MU_CONFIG_CMD_VERIFY,
+ MU_CONFIG_CMD_VIEW,
+
+ MU_CONFIG_CMD_NONE
+} MuConfigCmd;
+
+#define mu_config_cmd_is_valid(C) ((C) > MU_CONFIG_CMD_UNKNOWN && (C) < MU_CONFIG_CMD_NONE)
+
+/* struct with all configuration options for mu; it will be filled
+ * from the config file, and/or command line arguments */
+
+struct _MuConfig {
+ MuConfigCmd cmd; /* the command, or
+ * MU_CONFIG_CMD_NONE */
+ char* cmdstr; /* cmd string, for user
+ * info */
+ /* general options */
+ gboolean quiet; /* don't give any output */
+ gboolean debug; /* log debug-level info */
+ gchar* muhome; /* the House of Mu */
+ gboolean version; /* request mu version */
+ gboolean log_stderr; /* log to stderr (not logfile) */
+ gchar** params; /* parameters (for querying) */
+ gboolean nocolor; /* don't use use ansi-colors
+ * in some output */
+ gboolean verbose; /* verbose output */
+
+ /* options for init */
+ gchar* maildir; /* where the mails are */
+ char** my_addresses; /* 'my e-mail address', for mu cfind;
+ * can be use multiple times */
+ int max_msg_size; /* maximum size for message files */
+ int batch_size; /* database transaction batch size */
+
+ /* options for indexing */
+
+ gboolean nocleanup; /* don't cleanup del'd mails from db */
+ gboolean lazycheck; /* don't check dirs with up-to-date
+ * timestamps */
+
+ /* options for querying 'find' (and view-> 'summary') */
+ gchar* fields; /* fields to show in output */
+ gchar* sortfield; /* field to sort by (string) */
+ int maxnum; /* max # of entries to print */
+ gboolean reverse; /* sort in revers order (z->a) */
+ gboolean threads; /* show message threads */
+
+ int summary_len; /* max # of lines for summary */
+
+ gchar* bookmark; /* use bookmark */
+ gchar* formatstr; /* output type for find
+ * (plain,links,xml,json,sexp)
+ * and view (plain, sexp) and cfind
+ */
+ MuConfigFormat format; /* the decoded formatstr */
+ gchar* exec; /* command to execute on the
+ * files for the matched
+ * messages */
+ gboolean skip_dups; /* if there are multiple
+ * messages with the same
+ * msgid, show only the first
+ * one */
+ gboolean include_related; /* included related messages
+ * in results */
+ /* for find and cind */
+ time_t after; /* only show messages or
+ * addresses last seen after
+ * T */
+ /* options for crypto
+ * ie, 'view', 'extract' */
+ gboolean auto_retrieve; /* assume we're online */
+ gboolean decrypt; /* try to decrypt the
+ * message body, if any */
+
+ /* options for view */
+ gboolean terminator; /* add separator \f between
+ * multiple messages in mu
+ * view */
+
+ /* options for cfind (and 'find' --> "after") */
+ gboolean personal; /* only show 'personal' addresses */
+ /* also 'after' --> see above */
+
+ /* output to a maildir with symlinks */
+ gchar* linksdir; /* maildir to output symlinks */
+ gboolean clearlinks; /* clear a linksdir before filling */
+ mode_t dirmode; /* mode for the created maildir */
+
+ /* options for extracting parts */
+ gboolean save_all; /* extract all parts */
+ gboolean save_attachments; /* extract all attachment parts */
+ gchar* parts; /* comma-sep'd list of parts
+ * to save / open */
+ gchar* targetdir; /* where to save the attachments */
+ gboolean overwrite; /* should we overwrite same-named files */
+ gboolean play; /* after saving, try to 'play'
+ * (open) the attmnt using xdgopen */
+ /* for server */
+ gboolean commands; /* dump documentations for server
+ * commands */
+ gchar* eval; /* command to evaluate */
+
+ /* options for mu-script */
+ gchar* script; /* script to run */
+ const char** script_params; /* parameters for scripts */
+};
+typedef struct _MuConfig MuConfig;
+
+/**
+ * initialize a mu config object
+ *
+ * set default values for the configuration options; when you call
+ * mu_config_init, you should also call mu_config_uninit when the data
+ * is no longer needed.
+ *
+ * Note that this is _static_ data, ie., mu_config_init will always
+ * return the same pointer
+ *
+ * @param argcp: pointer to argc
+ * @param argvp: pointer to argv
+ * @param err: receives error information
+ */
+MuConfig* mu_config_init(int* argcp, char*** argvp, GError** err) G_GNUC_WARN_UNUSED_RESULT;
+/**
+ * free the MuConfig structure
+ *
+ * @param opts a MuConfig struct, or NULL
+ */
+void mu_config_uninit(MuConfig* conf);
+
+/**
+ * execute the command / options in this config
+ *
+ * @param opts a MuConfig struct
+ *
+ * @return a value denoting the success/failure of the execution;
+ * MU_ERROR_NONE (0) for success, non-zero for a failure. This is to used for
+ * the exit code of the process
+ *
+ */
+MuError mu_config_execute(const MuConfig* conf);
+
+/**
+ * count the number of non-option parameters
+ *
+ * @param opts a MuConfig struct
+ *
+ * @return the number of non-option parameters, or 0 in case of error
+ */
+size_t mu_config_param_num(const MuConfig* conf);
+
+/**
+ * determine Message::Options from command line args
+ *
+ * @param opts a MuConfig struct
+ *
+ * @return the corresponding Message::Options
+ */
+Message::Options mu_config_message_options(const MuConfig* opts);
+
+/**
+ * print help text for the current command
+ *
+ * @param cmd the command to show help for
+ */
+void mu_config_show_help(const MuConfigCmd cmd);
+
+} // namespace Mu.
+
+#endif /*__MU_CONFIG_H__*/
--- /dev/null
+## 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 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+## convert text blobs statements into c-strings
+
+BEGIN {
+ in_def=0;
+ in_string=0;
+ print "/* Do not edit - auto-generated. */"
+}
+
+
+/^#BEGIN/ {
+ printf "\tHelp {\n\t\t" $2 "," # e.g., MU_CONFIG_CMD_ADD
+ in_def=1
+}
+
+/^#STRING/ {
+ if (in_def== 1) {
+ if (in_string==1) {
+ print ",";
+ }
+ in_string=1
+ }
+}
+
+/^#END/ {
+ if (in_string==1) {
+ in_string=0;
+ }
+ in_def=0;
+ printf "\n\t},\n"
+}
+
+
+!/^#/ {
+ if (in_string==1) {
+ printf "\n\t\t\"" $0 "\\n\""
+ }
+}
+
+END {
+ print "/* the end */"
+}
--- /dev/null
+#-*-mode:org-*-
+#
+# 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 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+#BEGIN MU_CONFIG_CMD_ADD
+#STRING
+mu add <file> [<files>]
+#STRING
+mu add is the command to add specific measage files to the database. Each of the
+files must be specified with an absolute path.
+#END
+
+#BEGIN MU_CONFIG_CMD_CFIND
+#STRING
+mu cfind [options] [--format=<format>] [--personal] [--after=<T>] [<pattern>]
+#STRING
+mu cfind is the mu command to find contacts in the mu database and export them
+for use in other programs.
+
+<format> is one of:
+ plain
+ mutt-alias
+ mutt-ab
+ wl
+ csv
+ org-contact
+ bbdb
+
+'plain' is the default.
+
+If you specify '--personal', only addresses that were found in mails
+that include 'my' e-mail address will be listed - so to exclude e.g.
+mailing-list posts. Use the --my-address= option in 'mu index' to
+specify what addresses are considered 'my' address.
+
+With '--after=T' you can tell mu to only show addresses that were seen after
+T. T is a Unix timestamp. For example, to get only addresses seen after the
+beginning of 2012, you could use
+ --after=`date +%%s -d 2012-01-01`
+#END
+
+#BEGIN MU_CONFIG_CMD_EXTRACT
+#STRING
+mu extract [options] <file>
+#STRING
+mu extract is the mu command to display and save message parts
+(attachments), and open them with other tools.
+#END
+
+#BEGIN MU_CONFIG_CMD_FIELDS
+#STRING
+mu fields
+#STRING
+mu fields produces a table with all messages fields and flags. This
+is useful for writing query expressions.
+#END
+
+
+#BEGIN MU_CONFIG_CMD_FIND
+#STRING
+mu find [options] <search expression>
+#STRING
+mu find is the mu command for searching e-mail message that were
+stored earlier using mu index(1).
+
+Some examples:
+ # get all messages with 'bananas' in body, subject or recipient fields:
+ $ mu find bananas
+
+ # get all messages regarding bananas from John with an attachment:
+ $ mu find from:john flag:attach bananas
+
+ # get all messages with subject wombat in June 2009
+ $ mu find subject:wombat date:20090601..20090630
+
+See the `mu-find' and `mu-easy' man-pages for more information.
+#END
+
+#BEGIN MU_CONFIG_CMD_HELP
+#STRING
+mu help <command>
+#STRING
+mu help is the mu command to get help about <command>, where <command>
+is one of:
+ add - add message to database
+ cfind - find a contact
+ extract - extract parts/attachments from messages
+ fields - show table of all query fields and flags
+ find - query the message database
+ help - get help
+ index - index messages
+ init - init the mu database
+ mkdir - create a maildir
+ remove - remove a message from the database
+ script - run a script (available only when mu was built with guile-support)
+ server - start mu server
+ verify - verify signatures of a message
+ view - view a specific message
+#END
+
+#BEGIN MU_CONFIG_CMD_INDEX
+#STRING
+mu index [options]
+#STRING
+mu index is the mu command for scanning the contents of Maildir
+directories and storing the results in a Xapian database.The
+data can then be queried using mu-find(1).
+#END
+
+#BEGIN MU_CONFIG_CMD_INIT
+#STRING
+mu init [options]
+#STRING
+mu init is the mu command for setting up the mu database.
+#END
+
+#BEGIN MU_CONFIG_CMD_INFO
+#STRING
+mu init [options]
+#STRING
+mu info is the command for getting information about a mu database.
+#END
+
+#BEGIN MU_CONFIG_CMD_MKDIR
+#STRING
+mu mkdir [options] <dir> [<dirs>]
+#STRING
+mu mkdir is the command for creating Maildirs.It does not
+use the mu database.
+#END
+
+#BEGIN MU_CONFIG_CMD_REMOVE
+#STRING
+mu remove [options] <file> [<files>]
+#STRING
+mu remove is the mu command to remove messages from the database.
+#END
+
+#BEGIN MU_CONFIG_CMD_SERVER
+#STRING
+mu server [options]
+#STRING
+mu server starts a simple shell in which one can query and
+manipulate the mu database.The output of the commands is terms
+of Lisp symbolic expressions (s-exps). Its main use is for
+the mu4e e-mail client.
+#END
+
+#BEGIN MU_CONFIG_CMD_SCRIPT
+#STRING
+mu script [<pattern>] [-v]
+mu <script-name> [<script options>]
+#STRING
+
+List the available scripts and/or run them (if mu was built with support for
+scripts). With <pattern>, list only those scripts whose name or one-line
+description matches it. With -v, get a longer description for each script.
+
+Some examples:
+
+List all available scripts matching 'month' (long descriptions):
+ $ mu script -v month
+
+Run the 'msgs-per-month' script, and pass it the '--textonly' parameter:
+ $ mu msgs-per-month --textonly
+#END
+
+#BEGIN MU_CONFIG_CMD_VERIFY
+#STRING
+mu verify [options] <msgfile>
+#STRING
+mu verify is the mu command for verifying message signatures
+(such as PGP/GPG signatures)and displaying information about them.
+The command works on message files, and does not require
+the message to be indexed in the database.
+#END
+
+#BEGIN MU_CONFIG_CMD_VIEW
+#STRING
+mu view [options] <file> [<files>]
+#STRING
+mu view is the mu command for displaying e-mail message files. It
+works on message files and does not require the message to be
+indexed in the database.
+#END
--- /dev/null
+#!/bin/sh
+
+export G_SLICE=always-malloc
+export G_DEBUG=gc-friendly
+
+libtool --mode=execute valgrind --tool=memcheck --leak-check=full --show-possibly-lost=no --leak-resolution=med --track-origins=yes --num-callers=20 --log-file='@abs_top_builddir@/mu-%p.vgdump' @abs_top_builddir@/mu/mu $@
--- /dev/null
+/*
+** 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 <glib-object.h>
+#include <locale.h>
+
+#include "mu-config.hh"
+#include "mu-cmd.hh"
+#include "mu-runtime.hh"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+static void
+show_version(void)
+{
+ const char* blurb = "mu (mail indexer/searcher) version " VERSION "\n"
+ "Copyright (C) 2008-2022 Dirk-Jan C. Binnema\n"
+ "License GPLv3+: GNU GPL version 3 or later "
+ "<http://gnu.org/licenses/gpl.html>.\n"
+ "This is free software: you are free to change "
+ "and redistribute it.\n"
+ "There is NO WARRANTY, to the extent permitted by law.";
+
+ g_print("%s\n", blurb);
+}
+
+static int
+handle_result(const Result<void>& res, MuConfig* conf)
+{
+ if (res)
+ return 0;
+
+ using Color = MaybeAnsi::Color;
+ MaybeAnsi col{conf ? !conf->nocolor : false};
+
+ // show the error and some help, but not if it's only a softerror.
+ if (!res.error().is_soft_error()) {
+ std::cerr << col.fg(Color::Red) << "error" << col.reset() << ": "
+ << col.fg(Color::BrightYellow)
+ << res.error().what() << "\n";
+ } else
+ std::cerr << col.fg(Color::BrightBlue) << res.error().what() << '\n';
+
+ std::cerr << col.fg(Color::Green);
+
+ // perhaps give some useful hint on how to solve it.
+ switch (res.error().code()) {
+ case Error::Code::InvalidArgument:
+ if (conf && mu_config_cmd_is_valid(conf->cmd))
+ mu_config_show_help(conf->cmd);
+ break;
+ case Error::Code::StoreLock:
+ std::cerr << "Perhaps mu is already running?\n";
+ break;
+ case Error::Code::SchemaMismatch:
+ std::cerr << "Please (re)initialize mu with 'mu init' "
+ << "see mu-init(1) for details\n";
+ break;
+ default:
+ break; /* nothing to do */
+ }
+
+ std::cerr << col.reset();
+
+ return res.error().exit_code();
+}
+
+int
+main(int argc, char* argv[])
+{
+ int rv{};
+ MuConfig *conf{};
+ GError* err{};
+
+ using Color = MaybeAnsi::Color;
+ MaybeAnsi col{conf ? !conf->nocolor : false};
+
+ setlocale(LC_ALL, "");
+
+ conf = mu_config_init(&argc, &argv, &err);
+ if (!conf) {
+ std::cerr << col.fg(Color::Red) << "error" << col.reset() << ": "
+ << col.fg(Color::BrightYellow)
+ << (err ? err->message : "something went wrong") << "\n";
+ rv = 1;
+ goto cleanup;
+ } else if (conf->version) {
+ show_version();
+ goto cleanup;
+ } else if (conf->cmd == MU_CONFIG_CMD_NONE) /* nothing to do */
+ goto cleanup;
+ else if (!mu_runtime_init(conf->muhome, PACKAGE_NAME, conf->debug)) {
+ std::cerr << col.fg(Color::Red) << "error initializing mu\n"
+ << col.reset();
+ rv = 2;
+ } else
+ rv = handle_result(mu_cmd_execute(conf), conf);
+
+cleanup:
+ g_clear_error(&err);
+ mu_config_uninit(conf);
+ mu_runtime_uninit();
+
+ return rv;
+}
--- /dev/null
+/*
+** Copyright (C) 2011-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.
+**
+*/
+
+#define _POSIX_C_SOURCE 1
+
+#include <gmime/gmime.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <locale.h>
+
+static gchar*
+get_recip(GMimeMessage* msg, GMimeAddressType atype)
+{
+ char* recep;
+ InternetAddressList* receps;
+
+ receps = g_mime_message_get_addresses(msg, atype);
+ recep = (char*)internet_address_list_to_string(receps, NULL, FALSE);
+
+ if (!recep || !*recep) {
+ g_free(recep);
+ return NULL;
+ }
+
+ return recep;
+}
+
+static gchar*
+get_refs_str(GMimeMessage* msg)
+{
+ const gchar* str;
+ GMimeReferences* mime_refs;
+ int i, refs_len;
+ gchar* rv;
+
+ str = g_mime_object_get_header(GMIME_OBJECT(msg), "References");
+ if (!str)
+ return NULL;
+
+ mime_refs = g_mime_references_parse(NULL, str);
+ refs_len = g_mime_references_length(mime_refs);
+ for (rv = NULL, i = 0; i < refs_len; ++i) {
+ const char* msgid;
+ msgid = g_mime_references_get_message_id(mime_refs, i);
+ rv = g_strdup_printf("%s%s%s", rv ? rv : "", rv ? "," : "", msgid);
+ }
+ g_mime_references_free(mime_refs);
+
+ return rv;
+}
+
+static void
+print_date(GMimeMessage* msg)
+{
+ GDateTime* dt;
+ gchar* buf;
+
+ dt = g_mime_message_get_date(msg);
+ if (!dt)
+ return;
+
+ dt = g_date_time_to_local(dt);
+ buf = g_date_time_format(dt, "%c");
+ g_date_time_unref(dt);
+
+ if (buf) {
+ g_print("Date : %s\n", buf);
+ g_free(buf);
+ }
+}
+
+static void
+print_body(GMimeMessage* msg)
+{
+ GMimeObject* body;
+ GMimeDataWrapper* wrapper;
+ GMimeStream* stream;
+
+ body = g_mime_message_get_body(msg);
+
+ if (GMIME_IS_MULTIPART(body))
+ body = g_mime_multipart_get_part(GMIME_MULTIPART(body), 0);
+ if (!GMIME_IS_PART(body))
+ return;
+
+ wrapper = g_mime_part_get_content(GMIME_PART(body));
+ if (!GMIME_IS_DATA_WRAPPER(wrapper))
+ return;
+
+ stream = g_mime_data_wrapper_get_stream(wrapper);
+ if (!GMIME_IS_STREAM(stream))
+ return;
+
+ do {
+ char buf[512];
+ ssize_t len;
+
+ len = g_mime_stream_read(stream, buf, sizeof(buf));
+ if (len == -1)
+ break;
+
+ if (write(fileno(stdout), buf, len) == -1)
+ break;
+
+ if (len < (int)sizeof(buf))
+ break;
+
+ } while (1);
+}
+
+static gboolean
+test_message(GMimeMessage* msg)
+{
+ gchar* val;
+ const gchar* str;
+
+ val = get_recip(msg, GMIME_ADDRESS_TYPE_FROM);
+ g_print("From : %s\n", val ? val : "<none>");
+ g_free(val);
+
+ val = get_recip(msg, GMIME_ADDRESS_TYPE_TO);
+ g_print("To : %s\n", val ? val : "<none>");
+ g_free(val);
+
+ val = get_recip(msg, GMIME_ADDRESS_TYPE_CC);
+ g_print("Cc : %s\n", val ? val : "<none>");
+ g_free(val);
+
+ val = get_recip(msg, GMIME_ADDRESS_TYPE_BCC);
+ g_print("Bcc : %s\n", val ? val : "<none>");
+ g_free(val);
+
+ str = g_mime_message_get_subject(msg);
+ g_print("Subject: %s\n", str ? str : "<none>");
+
+ print_date(msg);
+
+ str = g_mime_message_get_message_id(msg);
+ g_print("Msg-id : %s\n", str ? str : "<none>");
+
+ {
+ gchar* refsstr;
+ refsstr = get_refs_str(msg);
+ g_print("Refs : %s\n", refsstr ? refsstr : "<none>");
+ g_free(refsstr);
+ }
+
+ print_body(msg);
+
+ return TRUE;
+}
+
+static gboolean
+test_stream(GMimeStream* stream)
+{
+ GMimeParser* parser;
+ GMimeMessage* msg;
+ gboolean rv;
+
+ parser = NULL;
+ msg = NULL;
+
+ parser = g_mime_parser_new_with_stream(stream);
+ if (!parser) {
+ g_warning("failed to create parser");
+ rv = FALSE;
+ goto leave;
+ }
+
+ msg = g_mime_parser_construct_message(parser, NULL);
+ if (!msg) {
+ g_warning("failed to construct message");
+ rv = FALSE;
+ goto leave;
+ }
+
+ rv = test_message(msg);
+
+leave:
+ if (parser)
+ g_object_unref(parser);
+
+ if (msg)
+ g_object_unref(msg);
+
+ return rv;
+}
+
+static gboolean
+test_file(const char* path)
+{
+ FILE* file;
+ GMimeStream* stream;
+ gboolean rv;
+
+ stream = NULL;
+ file = NULL;
+
+ file = fopen(path, "r");
+ if (!file) {
+ g_warning("cannot open file '%s': %s", path, g_strerror(errno));
+ rv = FALSE;
+ goto leave;
+ }
+
+ stream = g_mime_stream_file_new(file);
+ if (!stream) {
+ g_warning("cannot open stream for '%s'", path);
+ rv = FALSE;
+ goto leave;
+ }
+
+ rv = test_stream(stream);
+ g_object_unref(stream);
+ return rv;
+
+leave:
+ if (file)
+ fclose(file);
+
+ return rv;
+}
+
+int
+main(int argc, char* argv[])
+{
+ gboolean rv;
+
+ if (argc != 2) {
+ g_printerr("usage: %s <msg-file>\n", argv[0]);
+ return 1;
+ }
+
+ setlocale(LC_ALL, "");
+
+ g_mime_init();
+
+ rv = test_file(argv[1]);
+
+ g_mime_shutdown();
+
+ return rv ? 0 : 1;
+}
--- /dev/null
+## 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.
+
+#
+# tests
+#
+
+# BROKEN on ubuntu CI, but works elsewhere. Investigate
+test('test-cmd',
+ executable('test-cmd',
+ 'test-mu-cmd.cc',
+ install: false,
+ dependencies: [glib_dep, config_h_dep, lib_mu_dep]))
+
+test('test-cmd-cfind',
+ executable('test-cmd-cfind',
+ 'test-mu-cmd-cfind.cc',
+ install: false,
+ dependencies: [glib_dep, config_h_dep, lib_mu_dep]))
+test('test-cmd-query',
+ executable('test-cmd-query',
+ 'test-mu-query.cc',
+ install: false,
+ dependencies: [glib_dep, config_h_dep, lib_mu_dep]))
+
+gmime_test = executable(
+ 'gmime-test', [
+ 'gmime-test.c'
+],
+ dependencies: [ glib_dep, gmime_dep ],
+ install: false)
--- /dev/null
+/*
+**
+** 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.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-store.hh"
+#include "mu-query.hh"
+#include "utils/mu-utils.hh"
+
+static std::string CONTACTS_CACHE;
+
+using namespace Mu;
+
+static std::string
+fill_contacts_cache(const std::string& path)
+{
+ auto cmdline = format("/bin/sh -c '"
+ "%s init --muhome=%s --maildir=%s --quiet; "
+ "%s index --muhome=%s --quiet'",
+ MU_PROGRAM,
+ path.c_str(),
+ MU_TESTMAILDIR,
+ MU_PROGRAM,
+ path.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_assert(0);
+ }
+
+ return path;
+}
+
+static void
+test_mu_cfind_plain(void)
+{
+ gchar *cmdline, *output, *erroutput;
+
+ cmdline = g_strdup_printf("%s cfind --muhome=%s --format=plain "
+ "'testmu\\.xxx?'",
+ MU_PROGRAM,
+ CONTACTS_CACHE.c_str());
+ if (g_test_verbose())
+ g_print("%s\n", cmdline);
+
+ output = erroutput = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+ /* note, output order is unspecified */
+ g_assert(output);
+ if (output[0] == 'H')
+ g_assert_cmpstr(output,
+ ==,
+ "Helmut Kröger hk@testmu.xxx\n"
+ "Mü testmu@testmu.xx\n");
+ else
+ g_assert_cmpstr(output,
+ ==,
+ "Mü testmu@testmu.xx\n"
+ "Helmut Kröger hk@testmu.xxx\n");
+ g_free(cmdline);
+ g_free(output);
+ g_free(erroutput);
+}
+
+static void
+test_mu_cfind_bbdb(void)
+{
+ gchar * cmdline, *output, *erroutput, *expected;
+ gchar today[12];
+ struct tm* tmtoday;
+ time_t now;
+ const char* old_tz;
+
+ old_tz = set_tz("Europe/Helsinki");
+
+ cmdline = g_strdup_printf("%s cfind --muhome=%s --format=bbdb "
+ "'testmu\\.xxx?'",
+ MU_PROGRAM,
+ CONTACTS_CACHE.c_str());
+
+ output = erroutput = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+#define frm1 \
+ ";; -*-coding: utf-8-emacs;-*-\n" \
+ ";;; file-version: 6\n" \
+ "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") " \
+ "((creation-date . \"%s\") " \
+ "(time-stamp . \"1970-01-01\")) nil]\n" \
+ "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
+ "((creation-date . \"%s\") " \
+ "(time-stamp . \"1970-01-01\")) nil]\n"
+
+#define frm2 \
+ ";; -*-coding: utf-8-emacs;-*-\n" \
+ ";;; file-version: 6\n" \
+ "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
+ "((creation-date . \"%s\") " \
+ "(time-stamp . \"1970-01-01\")) nil]\n" \
+ "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") " \
+ "((creation-date . \"%s\") " \
+ "(time-stamp . \"1970-01-01\")) nil]\n"
+
+ g_assert(output);
+
+ now = time(NULL);
+ tmtoday = localtime(&now);
+ strftime(today, sizeof(today), "%Y-%m-%d", tmtoday);
+
+ expected = g_strdup_printf(output[52] == 'H' ? frm1 : frm2, today, today);
+
+ /* g_print ("\n%s\n", output); */
+
+ g_assert_cmpstr(output, ==, expected);
+
+ g_free(cmdline);
+ g_free(output);
+ g_free(erroutput);
+ g_free(expected);
+
+ set_tz(old_tz);
+}
+
+static void
+test_mu_cfind_wl(void)
+{
+ gchar *cmdline, *output, *erroutput;
+
+ cmdline = g_strdup_printf("%s cfind --muhome=%s --format=wl "
+ "'testmu\\.xxx?'",
+ MU_PROGRAM,
+ CONTACTS_CACHE.c_str());
+
+ output = erroutput = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+ g_assert(output);
+ if (output[0] == 'h')
+ g_assert_cmpstr(output,
+ ==,
+ "hk@testmu.xxx \"HelmutK\" \"Helmut Kröger\"\n"
+ "testmu@testmu.xx \"Mü\" \"Mü\"\n");
+ else
+ g_assert_cmpstr(output,
+ ==,
+ "testmu@testmu.xx \"Mü\" \"Mü\"\n"
+ "hk@testmu.xxx \"HelmutK\" \"Helmut Kröger\"\n");
+
+ g_free(cmdline);
+ g_free(output);
+ g_free(erroutput);
+}
+
+static void
+test_mu_cfind_mutt_alias(void)
+{
+ gchar *cmdline, *output, *erroutput;
+
+ cmdline = g_strdup_printf("%s cfind --muhome=%s --format=mutt-alias "
+ "'testmu\\.xxx?'",
+ MU_PROGRAM,
+ CONTACTS_CACHE.c_str());
+
+ output = erroutput = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+ /* both orders are possible... */
+ g_assert(output);
+
+ if (output[6] == 'H')
+ g_assert_cmpstr(output,
+ ==,
+ "alias HelmutK Helmut Kröger <hk@testmu.xxx>\n"
+ "alias Mü Mü <testmu@testmu.xx>\n");
+ else
+ g_assert_cmpstr(output,
+ ==,
+ "alias Mü Mü <testmu@testmu.xx>\n"
+ "alias HelmutK Helmut Kröger <hk@testmu.xxx>\n");
+
+ g_free(cmdline);
+ g_free(output);
+ g_free(erroutput);
+}
+
+static void
+test_mu_cfind_mutt_ab(void)
+{
+ gchar *cmdline, *output, *erroutput;
+
+ cmdline = g_strdup_printf("%s cfind --muhome=%s --format=mutt-ab "
+ "'testmu\\.xxx?'",
+ MU_PROGRAM,
+ CONTACTS_CACHE.c_str());
+
+ if (g_test_verbose())
+ g_print("%s\n", cmdline);
+
+ output = erroutput = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ g_assert(output);
+
+ if (output[39] == 'h')
+ g_assert_cmpstr(output,
+ ==,
+ "Matching addresses in the mu database:\n"
+ "hk@testmu.xxx\tHelmut Kröger\t\n"
+ "testmu@testmu.xx\tMü\t\n");
+ else
+ g_assert_cmpstr(output,
+ ==,
+ "Matching addresses in the mu database:\n"
+ "testmu@testmu.xx\tMü\t\n"
+ "hk@testmu.xxx\tHelmut Kröger\t\n");
+
+ g_free(cmdline);
+ g_free(output);
+ g_free(erroutput);
+}
+
+static void
+test_mu_cfind_org_contact(void)
+{
+ gchar *cmdline, *output, *erroutput;
+
+ cmdline = g_strdup_printf("%s cfind --muhome=%s --format=org-contact "
+ "'testmu\\.xxx?'",
+ MU_PROGRAM,
+ CONTACTS_CACHE.c_str());
+
+ output = erroutput = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+ g_assert(output);
+
+ if (output[2] == 'H')
+ g_assert_cmpstr(output,
+ ==,
+ "* Helmut Kröger\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: hk@testmu.xxx\n"
+ ":END:\n\n"
+ "* Mü\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: testmu@testmu.xx\n"
+ ":END:\n\n");
+ else
+ g_assert_cmpstr(output,
+ ==,
+ "* Mü\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: testmu@testmu.xx\n"
+ ":END:\n\n"
+ "* Helmut Kröger\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: hk@testmu.xxx\n"
+ ":END:\n\n");
+
+ g_free(cmdline);
+ g_free(output);
+ g_free(erroutput);
+}
+
+static void
+test_mu_cfind_csv(void)
+{
+ gchar *cmdline, *output, *erroutput;
+
+ cmdline = g_strdup_printf("%s cfind --muhome=%s --format=csv "
+ "'testmu\\.xxx?'",
+ MU_PROGRAM,
+ CONTACTS_CACHE.c_str());
+
+ if (g_test_verbose())
+ g_print("%s\n", cmdline);
+
+ output = erroutput = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ g_assert(output);
+ if (output[1] == 'H')
+ g_assert_cmpstr(output,
+ ==,
+ "\"Helmut Kröger\",\"hk@testmu.xxx\"\n"
+ "\"Mü\",\"testmu@testmu.xx\"\n");
+ else
+ g_assert_cmpstr(output,
+ ==,
+ "\"Mü\",\"testmu@testmu.xx\"\n"
+ "\"Helmut Kröger\",\"hk@testmu.xxx\"\n");
+ g_free(cmdline);
+ g_free(output);
+ g_free(erroutput);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ if (!set_en_us_utf8_locale())
+ return 0; /* don't error out... */
+
+ TempDir tmpdir{};
+ CONTACTS_CACHE = fill_contacts_cache(tmpdir.path());
+
+ g_test_add_func("/mu-cmd-cfind/test-mu-cfind-plain", test_mu_cfind_plain);
+ g_test_add_func("/mu-cmd-cfind/test-mu-cfind-bbdb", test_mu_cfind_bbdb);
+ g_test_add_func("/mu-cmd-cfind/test-mu-cfind-wl", test_mu_cfind_wl);
+ g_test_add_func("/mu-cmd-cfind/test-mu-cfind-mutt-alias", test_mu_cfind_mutt_alias);
+ g_test_add_func("/mu-cmd-cfind/test-mu-cfind-mutt-ab", test_mu_cfind_mutt_ab);
+ g_test_add_func("/mu-cmd-cfind/test-mu-cfind-org-contact", test_mu_cfind_org_contact);
+ g_test_add_func("/mu-cmd-cfind/test-mu-cfind-csv", test_mu_cfind_csv);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** 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 <glib/gstdio.h>
+#include <string.h>
+#include <errno.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-store.hh"
+#include "mu-query.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+/* tests for the command line interface, uses testdir2 */
+
+static std::string DBPATH; /* global */
+
+static void
+fill_database(void)
+{
+ gchar * cmdline;
+ GError* err;
+
+ cmdline = g_strdup_printf("/bin/sh -c '"
+ "%s init --muhome=%s --maildir=%s --quiet; "
+ "%s index --muhome=%s --quiet'",
+ MU_PROGRAM,
+ DBPATH.c_str(),
+ MU_TESTMAILDIR2,
+ MU_PROGRAM,
+ DBPATH.c_str());
+ if (g_test_verbose())
+ g_print("%s\n", cmdline);
+
+ err = NULL;
+ if (!g_spawn_command_line_sync(cmdline, NULL, NULL, NULL, &err)) {
+ g_printerr("Error: %s\n", err ? err->message : "?");
+ g_assert(0);
+ }
+
+ g_free(cmdline);
+}
+
+static unsigned
+newlines_in_output(const char* str)
+{
+ int count;
+
+ count = 0;
+
+ while (str && *str) {
+ if (*str == '\n')
+ ++count;
+ ++str;
+ }
+
+ return count;
+}
+
+static size_t
+search_func(const char* query, unsigned expected)
+{
+ size_t lines;
+ gchar *cmdline, *output, *erroutput;
+
+ cmdline = g_strdup_printf("%s find --muhome=%s %s", MU_PROGRAM,
+ DBPATH.c_str(), query);
+
+ g_message("[%u] %s", expected, query);
+
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ if (g_test_verbose())
+ g_print("\nOutput:\n%s", output);
+
+ lines = newlines_in_output(output);
+
+ /* we expect zero lines of error output if there is a match;
+ * otherwise there should be one line 'No matches found' */
+ /* g_assert_cmpuint (newlines_in_output(erroutput),==, */
+ /* expected == 0 ? 1 : 0); */
+
+ g_free(output);
+ g_free(erroutput);
+ g_free(cmdline);
+
+ return lines;
+}
+
+#define search(Q,EXP) do { \
+ unsigned lines = search_func(Q, EXP); \
+ g_assert_cmpuint(lines, ==, EXP); \
+} while(0)
+
+
+/* index testdir2, and make sure it adds two documents */
+static void
+test_mu_index(void)
+{
+ auto store = Store::make(DBPATH + "/xapian");
+ assert_valid_result(store);
+ g_assert_cmpuint(store->size(), ==, 13);
+}
+
+static void
+test_mu_find_empty_query(void)
+{
+ search("\"\"", 13);
+}
+
+static void
+test_mu_find_01(void)
+{
+ search("f:john fruit", 1);
+ search("f:soc@example.com", 1);
+ search("t:alki@example.com", 1);
+ search("t:alcibiades", 1);
+ search("http emacs", 1);
+ search("f:soc@example.com OR f:john", 2);
+ search("f:soc@example.com OR f:john OR t:edmond", 3);
+ search("t:julius", 1);
+ search("s:dude", 1);
+ search("t:dantès", 1);
+}
+
+/* index testdir2, and make sure it adds two documents */
+static void
+test_mu_find_02(void)
+{
+ search("bull", 1);
+ search("bull m:foo", 0);
+ search("bull m:/foo", 1);
+ search("bull m:/Foo", 1);
+ search("bull flag:attach", 1);
+ search("bull flag:a", 1);
+ search("g:x", 0);
+ search("flag:encrypted", 0);
+ search("flag:attach", 1);
+}
+
+static void
+test_mu_find_file(void)
+{
+ search("file:sittingbull.jpg", 1);
+ search("file:custer.jpg", 1);
+ search("file:custer.*", 1);
+ search("j:sit*", 1);
+}
+
+static void
+test_mu_find_mime(void)
+{
+ search("mime:image/jpeg", 1);
+ search("mime:text/plain", 13);
+ search("y:text*", 13);
+ search("y:image*", 1);
+ search("mime:message/rfc822", 2);
+}
+
+static void
+test_mu_find_text_in_rfc822(void)
+{
+ search("embed:dancing", 1);
+ search("e:curious", 1);
+ search("embed:with", 2);
+ search("e:karjala", 0);
+ search("embed:navigation", 1);
+}
+
+/* some more tests */
+static void
+test_mu_find_03(void)
+{
+ search("bull", 1);
+ search("bull m:foo", 0);
+ search("bull m:/foo", 1);
+ search("i:3BE9E6535E0D852173@emss35m06.us.lmco.com", 1);
+}
+
+static void /* error cases */
+test_mu_find_04(void)
+{
+ gchar *cmdline, *erroutput;
+
+ cmdline = g_strdup_printf("find %s --muhome=%cfoo%cbar%cnonexistent "
+ "f:socrates",
+ MU_PROGRAM,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+
+ g_assert(g_spawn_command_line_sync(cmdline, NULL, &erroutput, NULL, NULL));
+
+ /* we expect multiple lines of error output */
+ g_assert_cmpuint(newlines_in_output(erroutput), >=, 1);
+
+ g_free(erroutput);
+ g_free(cmdline);
+}
+
+G_GNUC_UNUSED static void
+test_mu_find_links(void)
+{
+ gchar *cmdline, *output, *erroutput, *tmpdir;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+
+ cmdline = g_strdup_printf("%s find --muhome=%s --format=links --linksdir=%s "
+ "mime:message/rfc822",
+ MU_PROGRAM,
+ DBPATH.c_str(),
+ tmpdir);
+
+ if (g_test_verbose())
+ g_print("cmdline: %s\n", cmdline);
+
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ /* there should be no errors */
+ g_assert_cmpuint(newlines_in_output(output), ==, 0);
+ g_assert_cmpuint(newlines_in_output(erroutput), ==, 0);
+ g_free(output);
+ g_free(erroutput);
+
+ /* now we try again, we should get a line of error output,
+ * when we find the first target file already exists */
+
+ if (g_test_verbose())
+ g_print("cmdline: %s\n", cmdline);
+
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ g_assert_cmpuint(newlines_in_output(output), ==, 0);
+ g_assert_cmpuint(newlines_in_output(erroutput), ==, 1);
+ g_free(output);
+ g_free(erroutput);
+
+ /* now we try again with --clearlinks, and the we should be
+ * back to 0 errors */
+ g_free(cmdline);
+ cmdline = g_strdup_printf("%s find --muhome=%s --format=links --linksdir=%s --clearlinks "
+ "mime:message/rfc822",
+ MU_PROGRAM,
+ DBPATH.c_str(),
+ tmpdir);
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ if (g_test_verbose())
+ g_print("cmdline: %s\n", cmdline);
+ g_assert_cmpuint(newlines_in_output(output), ==, 0);
+ g_assert_cmpuint(newlines_in_output(erroutput), ==, 0);
+ g_free(output);
+ g_free(erroutput);
+
+ g_free(cmdline);
+ g_free(tmpdir);
+}
+
+/* some more tests */
+static void
+test_mu_find_maildir_special(void)
+{
+ search("\"maildir:/wOm_bàT\"", 3);
+ search("\"maildir:/wOm*\"", 3);
+ search("\"maildir:/wOm_*\"", 3);
+ search("\"maildir:wom_bat\"", 0);
+ search("\"maildir:/wombat\"", 0);
+ search("subject:atoms", 1);
+ search("\"maildir:/wom_bat\" subject:atoms", 1);
+}
+
+/* static void */
+/* test_mu_find_mime_types (void) */
+/* { */
+/* /\* ensure that maldirs with spaces in their names work... *\/ */
+/* search ("\"maildir:/wom bat\" subject:atoms", 1); */
+/* search ("\"maildir:/wOm_bàT\"", 3); */
+/* search ("subject:atoms", 1); */
+/* } */
+
+static void
+test_mu_extract_01(void)
+{
+ gchar *cmdline, *output, *erroutput, *tmpdir;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s extract --muhome=%s %s%cFoo%ccur%cmail5",
+ MU_PROGRAM,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+
+ if (g_test_verbose())
+ g_print("cmd: %s\n", cmdline);
+
+ output = erroutput = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+ // ERROR:test-mu-cmd.cc:347:void test_mu_extract_01(): assertion failed (output ==
+ // "MIME-parts in this message:\n" " 1 <none> text/plain [<none>] (27 bytes)\n" " 2
+ // sittingbull.jpg image/jpeg [inline] (23.9\302\240kB)\n" " 3 custer.jpg image/jpeg
+ // [inline] (21.6\302\240kB)\n"):
+ // ("MIME-parts in this message:\n 1 <none> text/plain [<none>] (27 bytes)\n 2
+ // sittingbull.jpg image/jpeg [inline] (23.9 kB)\n 3 custer.jpg image/jpeg [inline] (21.6
+ // kB)\n" == "MIME-parts in this message:\n 1 <none> text/plain [<none>] (27 bytes)\n 2
+ // sittingbull.jpg image/jpeg [inline] (23.9\302\240kB)\n 3 custer.jpg image/jpeg [inline]
+ // (21.6\302\240kB)\n")
+ /* g_assert_cmpstr (output, */
+ /* ==, */
+ /* "MIME-parts in this message:\n" */
+ /* " 1 <none> text/plain [<none>] (27 bytes)\n" */
+ /* " 2 sittingbull.jpg image/jpeg [inline] (23.9\302\240kB)\n" */
+ /* " 3 custer.jpg image/jpeg [inline] (21.6\302\240kB)\n"); */
+
+ /* we expect zero lines of error output */
+ g_assert_cmpuint(newlines_in_output(erroutput), ==, 0);
+
+ g_free(output);
+ g_free(erroutput);
+ g_free(cmdline);
+ g_free(tmpdir);
+}
+
+static gint64
+get_file_size(const char* path)
+{
+ int rv;
+ struct stat statbuf;
+
+ rv = stat(path, &statbuf);
+ if (rv != 0) {
+ /* g_warning ("error: %s", g_strerror (errno)); */
+ return -1;
+ }
+
+ return (gint64)statbuf.st_size;
+}
+
+static void
+test_mu_extract_02(void)
+{
+ gchar *cmdline, *output, *tmpdir;
+ gchar *att1, *att2;
+ size_t size;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s extract --muhome=%s -a "
+ "--target-dir=%s %s%cFoo%ccur%cmail5",
+ MU_PROGRAM,
+ tmpdir,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ output = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+ g_assert_cmpstr(output, ==, "");
+
+ att1 = g_strdup_printf("%s%ccuster.jpg", tmpdir, G_DIR_SEPARATOR);
+ att2 = g_strdup_printf("%s%csittingbull.jpg", tmpdir, G_DIR_SEPARATOR);
+
+ size = get_file_size(att1);
+ g_assert_cmpuint(size, >=, 15955);
+ g_assert_cmpuint(size, <=, 15960);
+
+ size = get_file_size(att2);
+ g_assert_cmpint(size, ==, 17674);
+
+
+ g_free(output);
+ g_free(tmpdir);
+ g_free(cmdline);
+ g_free(att1);
+ g_free(att2);
+}
+
+static void
+test_mu_extract_03(void)
+{
+ gchar *cmdline, *output, *tmpdir;
+ gchar *att1, *att2;
+ size_t size;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s extract --muhome=%s --parts 3 "
+ "--target-dir=%s %s%cFoo%ccur%cmail5",
+ MU_PROGRAM,
+ tmpdir,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+ output = NULL;
+
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+ g_assert_cmpstr(output, ==, "");
+
+ att1 = g_strdup_printf("%s%ccuster.jpg", tmpdir, G_DIR_SEPARATOR);
+ att2 = g_strdup_printf("%s%csittingbull.jpg", tmpdir, G_DIR_SEPARATOR);
+
+ size = get_file_size(att1);
+ g_assert_cmpuint(size, >=, 15955);
+ g_assert_cmpuint(size, <=, 15960);
+ g_assert_cmpint(get_file_size(att2), ==, -1);
+
+ g_free(output);
+ g_free(tmpdir);
+ g_free(cmdline);
+ g_free(att1);
+ g_free(att2);
+}
+
+static void
+test_mu_extract_overwrite(void)
+{
+ gchar *cmdline, *output, *erroutput, *tmpdir;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s extract --muhome=%s -a "
+ "--target-dir=%s %s%cFoo%ccur%cmail5",
+ MU_PROGRAM,
+ tmpdir,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ g_assert_cmpstr(output, ==, "");
+ g_assert_cmpstr(erroutput, ==, "");
+ g_free(erroutput);
+ g_free(output);
+
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ /* now, it should fail, because we don't allow overwrites
+ * without --overwrite */
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ g_assert_cmpstr(output, ==, "");
+ g_assert_cmpstr(erroutput, !=, "");
+ g_free(erroutput);
+ g_free(output);
+
+ g_free(cmdline);
+ /* this should work now, because we have specified --overwrite */
+ cmdline = g_strdup_printf("%s extract --muhome=%s -a --overwrite "
+ "--target-dir=%s %s%cFoo%ccur%cmail5",
+ MU_PROGRAM,
+ tmpdir,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ g_assert_cmpstr(output, ==, "");
+ g_assert_cmpstr(erroutput, ==, "");
+ g_free(erroutput);
+ g_free(output);
+
+ g_free(tmpdir);
+ g_free(cmdline);
+}
+
+static void
+test_mu_extract_by_name(void)
+{
+ gchar *cmdline, *output, *erroutput, *tmpdir, *path;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s extract --muhome=%s "
+ "--target-dir=%s %s%cFoo%ccur%cmail5 "
+ "sittingbull.jpg",
+ MU_PROGRAM,
+ tmpdir,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+ g_assert_cmpstr(output, ==, "");
+ g_assert_cmpstr(erroutput, ==, "");
+ path = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "sittingbull.jpg");
+ g_assert(access(path, F_OK) == 0);
+ g_free(path);
+
+ g_free(erroutput);
+ g_free(output);
+
+ g_free(tmpdir);
+ g_free(cmdline);
+}
+
+static void
+test_mu_view_01(void)
+{
+ gchar *cmdline, *output, *tmpdir;
+ int len;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s view --muhome=%s %s%cbar%ccur%cmail4",
+ MU_PROGRAM,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+ output = NULL;
+
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+ g_assert_cmpstr(output, !=, NULL);
+
+ /*
+ * note: there are two possibilities here; older versions of
+ * GMime will produce:
+ *
+ * From: "=?iso-8859-1?Q? =F6tzi ?=" <oetzi@web.de>
+ *
+ * while newer ones return something like:
+ *
+ * From: ?tzi <oetzi@web.de>
+ *
+ * or even
+ *
+ * From: \xc3\xb6tzi <oetzi@web.de>
+ *
+ * both are 'okay' from mu's perspective; it'd be even better
+ * to have some #ifdefs for the GMime versions, but this
+ * should work for now
+ *
+ * Added 350 as 'okay', which comes with gmime 2.4.24 (ubuntu 10.04)
+ */
+ len = strlen(output);
+ if (len < 339) {
+ g_print ("\n[%s] (%d)\n", output, len);
+ }
+ g_assert_cmpuint(len, >=, 339);
+ g_free(output);
+ g_free(cmdline);
+ g_free(tmpdir);
+}
+
+static void
+test_mu_view_multi(void)
+{
+ gchar *cmdline, *output, *tmpdir;
+ int len;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s view --muhome=%s "
+ "%s%cbar%ccur%cmail5 "
+ "%s%cbar%ccur%cmail5",
+ MU_PROGRAM,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+ output = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+ g_assert_cmpstr(output, !=, NULL);
+
+ len = strlen(output);
+ if (g_test_verbose())
+ g_print ("\n[%s](%u)\n", output, len);
+ g_assert_cmpuint(len, >=, 112);
+
+ g_free(output);
+ g_free(cmdline);
+ g_free(tmpdir);
+}
+
+static void
+test_mu_view_multi_separate(void)
+{
+ gchar *cmdline, *output, *tmpdir;
+ int len;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s view --terminate --muhome=%s "
+ "%s%cbar%ccur%cmail5 "
+ "%s%cbar%ccur%cmail5",
+ MU_PROGRAM,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+ output = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+ g_assert_cmpstr(output, !=, NULL);
+
+ len = strlen(output);
+ if (g_test_verbose())
+ g_print ("\n[%s](%u)\n", output, len);
+ g_print ("\n[%s](%u)\n", output, len);
+ //g_assert_cmpuint(len, >=, 112);
+
+ g_free(output);
+ g_free(cmdline);
+ g_free(tmpdir);
+}
+
+static void
+test_mu_view_attach(void)
+{
+ gchar *cmdline, *output, *tmpdir;
+ int len;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s view --muhome=%s %s%cFoo%ccur%cmail5",
+ MU_PROGRAM,
+ tmpdir,
+ MU_TESTMAILDIR2,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR,
+ G_DIR_SEPARATOR);
+ output = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+ g_assert_cmpstr(output, !=, NULL);
+
+ len = strlen(output);
+ if (g_test_verbose())
+ g_print ("\n[%s](%u)\n", output, len);
+ //g_assert(len == 168 || len == 166);
+
+ g_free(output);
+ g_free(cmdline);
+ g_free(tmpdir);
+}
+
+static void
+test_mu_mkdir_01(void)
+{
+ gchar *cmdline, *output, *tmpdir;
+ gchar* dir;
+
+ tmpdir = test_mu_common_get_random_tmpdir();
+ g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+ cmdline = g_strdup_printf("%s mkdir --muhome=%s %s%ctest1 %s%ctest2",
+ MU_PROGRAM,
+ tmpdir,
+ tmpdir,
+ G_DIR_SEPARATOR,
+ tmpdir,
+ G_DIR_SEPARATOR);
+
+ output = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+ g_assert_cmpstr(output, ==, "");
+
+ dir = g_strdup_printf("%s%ctest1%ccur", tmpdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+ g_assert(access(dir, F_OK) == 0);
+ g_free(dir);
+
+ dir = g_strdup_printf("%s%ctest2%ctmp", tmpdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+ g_assert(access(dir, F_OK) == 0);
+ g_free(dir);
+
+ dir = g_strdup_printf("%s%ctest1%cnew", tmpdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+ g_assert(access(dir, F_OK) == 0);
+ g_free(dir);
+
+ g_free(output);
+ g_free(tmpdir);
+ g_free(cmdline);
+}
+
+/* we can only test 'verify' if gpg is installed, and has
+ * djcb@djcbsoftware's key in the keyring */
+G_GNUC_UNUSED static gboolean
+verify_is_testable(void)
+{
+ gchar * gpg, *cmdline;
+ gchar * output, *erroutput;
+ int retval;
+ gboolean rv;
+
+ /* find GPG or return FALSE */
+ if ((gpg = (char*)g_getenv("MU_GPG_PATH"))) {
+ if (access(gpg, X_OK) != 0)
+ return FALSE;
+ else
+ gpg = g_strdup(gpg);
+
+ } else if (!(gpg = g_find_program_in_path("gpg2")))
+ return FALSE;
+
+ cmdline = g_strdup_printf("%s --list-keys DCC4A036", gpg);
+ g_free(gpg);
+
+ output = erroutput = NULL;
+ rv = g_spawn_command_line_sync(cmdline, &output, &erroutput, &retval, NULL);
+ g_free(output);
+ g_free(erroutput);
+ g_free(cmdline);
+
+ return (rv && retval == 0) ? TRUE : FALSE;
+}
+
+G_GNUC_UNUSED static void
+test_mu_verify_good(void)
+{
+ gchar *cmdline, *output;
+ int retval;
+
+ if (!verify_is_testable())
+ return;
+
+ cmdline = g_strdup_printf("%s verify '%s/signed!2,S'", MU_PROGRAM, MU_TESTMAILDIR4);
+
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ output = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, &retval, NULL));
+ g_free(output);
+ g_assert_cmpuint(retval, ==, 0);
+ g_free(cmdline);
+}
+
+G_GNUC_UNUSED static void
+test_mu_verify_bad(void)
+{
+ gchar *cmdline, *output;
+ int retval;
+
+ if (!verify_is_testable())
+ return;
+
+ cmdline = g_strdup_printf("%s verify '%s/signed-bad!2,S'", MU_PROGRAM, MU_TESTMAILDIR4);
+
+ if (g_test_verbose())
+ g_print("$ %s\n", cmdline);
+
+ output = NULL;
+ g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, &retval, NULL));
+ g_free(output);
+ g_assert_cmpuint(retval, !=, 0);
+ g_free(cmdline);
+}
+
+int
+main(int argc, char* argv[])
+{
+ int rv;
+
+ /* currently, something is broken on Ubuntu CI (but not elsewhere);
+ * selectively turn this test off */
+ if (!g_getenv("RUN_TEST_MU_CMD"))
+ return 0;
+
+ mu_test_init(&argc, &argv);
+
+ if (!set_en_us_utf8_locale())
+ return 0; /* don't error out... */
+
+ g_test_add_func("/mu-cmd/test-mu-index", test_mu_index);
+
+ g_test_add_func("/mu-cmd/test-mu-find-empty-query", test_mu_find_empty_query);
+ g_test_add_func("/mu-cmd/test-mu-find-01", test_mu_find_01);
+ g_test_add_func("/mu-cmd/test-mu-find-02", test_mu_find_02);
+
+ g_test_add_func("/mu-cmd/test-mu-find-file", test_mu_find_file);
+ g_test_add_func("/mu-cmd/test-mu-find-mime", test_mu_find_mime);
+
+ /* recently, this test breaks _sometimes_ when run on Travis; but it
+ * seems related to the setup there, as nothing has changed in the code.
+ * turn off for now. */
+ /* g_test_add_func ("/mu-cmd/test-mu-find-links",
+ * test_mu_find_links); */
+
+ g_test_add_func("/mu-cmd/test-mu-find-text-in-rfc822", test_mu_find_text_in_rfc822);
+
+ g_test_add_func("/mu-cmd/test-mu-find-03", test_mu_find_03);
+ g_test_add_func("/mu-cmd/test-mu-find-04", test_mu_find_04);
+ g_test_add_func("/mu-cmd/test-mu-find-maildir-special", test_mu_find_maildir_special);
+ g_test_add_func("/mu-cmd/test-mu-extract-01", test_mu_extract_01);
+ g_test_add_func("/mu-cmd/test-mu-extract-02", test_mu_extract_02);
+ g_test_add_func("/mu-cmd/test-mu-extract-03", test_mu_extract_03);
+ g_test_add_func("/mu-cmd/test-mu-extract-overwrite", test_mu_extract_overwrite);
+ g_test_add_func("/mu-cmd/test-mu-extract-by-name", test_mu_extract_by_name);
+
+ g_test_add_func("/mu-cmd/test-mu-view-01", test_mu_view_01);
+ g_test_add_func("/mu-cmd/test-mu-view-multi", test_mu_view_multi);
+ g_test_add_func("/mu-cmd/test-mu-view-multi-separate", test_mu_view_multi_separate);
+ g_test_add_func("/mu-cmd/test-mu-view-attach", test_mu_view_attach);
+ g_test_add_func("/mu-cmd/test-mu-mkdir-01", test_mu_mkdir_01);
+
+ g_test_add_func("/mu-cmd/test-mu-verify-good", test_mu_verify_good);
+ g_test_add_func("/mu-cmd/test-mu-verify-bad", test_mu_verify_bad);
+
+ TempDir tempdir;
+ DBPATH = tempdir.path();
+ fill_database();
+
+ rv = g_test_run();
+
+ return rv;
+}
--- /dev/null
+/*
+** 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 <unordered_set>
+#include <string>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-query.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+#include "mu-store.hh"
+
+using namespace Mu;
+
+static std::string DB_PATH1;
+static std::string DB_PATH2;
+
+static std::string
+make_database(const std::string& testdir)
+{
+ char* tmpdir{test_mu_common_get_random_tmpdir()};
+
+ /* use the env var rather than `--muhome` */
+
+ g_setenv("MUHOME", tmpdir, 1);
+ const auto cmdline{format("/bin/sh -c '"
+ "%s init --maildir=%s --quiet ; "
+ "%s index --quiet'",
+ MU_PROGRAM,
+ testdir.c_str(),
+ MU_PROGRAM)};
+
+ if (g_test_verbose())
+ g_printerr("\n%s\n", cmdline.c_str());
+
+ g_assert(g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, NULL, NULL));
+ auto xpath = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "xapian");
+ g_free(tmpdir);
+
+ /* ensure MUHOME worked */
+ g_assert_cmpuint(::access(xpath, F_OK), ==, 0);
+
+ std::string dbpath{xpath};
+ g_free(xpath);
+
+ return dbpath;
+}
+
+static void
+assert_no_dups(const QueryResults& qres)
+{
+ std::unordered_set<std::string> msgid_set, path_set;
+
+ for (auto&& mi : qres) {
+ g_assert_true(msgid_set.find(mi.message_id().value()) == msgid_set.end());
+ g_assert_true(path_set.find(mi.path().value()) == path_set.end());
+
+ path_set.emplace(*mi.path());
+ msgid_set.emplace(*mi.message_id());
+
+ g_assert_false(msgid_set.find(mi.message_id().value()) == msgid_set.end());
+ g_assert_false(path_set.find(mi.path().value()) == path_set.end());
+ }
+}
+
+/* note: this also *moves the iter* */
+static size_t
+run_and_count_matches(const std::string& xpath,
+ const std::string& expr,
+ Mu::QueryFlags flags = Mu::QueryFlags::None)
+{
+ auto store{Store::make(xpath)};
+ assert_valid_result(store);
+
+ // if (g_test_verbose()) {
+ // std::cout << "==> mquery: " << store.parse_query(expr, false) << "\n";
+ // std::cout << "==> xquery: " << store.parse_query(expr, true) << "\n";
+ // }
+
+ Mu::allow_warnings();
+
+ auto qres{store->run_query(expr, {}, flags)};
+ g_assert_true(!!qres);
+ assert_no_dups(*qres);
+
+
+ if (g_test_verbose())
+ g_print("'%s' => %zu\n", expr.c_str(), qres->size());
+
+ return qres->size();
+}
+
+typedef struct {
+ const char* query;
+ size_t count; /* expected number of matches */
+} QResults;
+
+static void
+test_mu_query_01(void)
+{
+ int i;
+ QResults queries[] = {
+ {"basic", 3},
+ {"question", 5},
+ {"thanks", 2},
+ {"html", 4},
+ {"subject:exception", 1},
+ {"exception", 1},
+ {"subject:A&B", 1},
+ {"A&B", 1},
+ {"subject:elisp", 1},
+ {"html AND contains", 1},
+ {"html and contains", 1},
+ {"from:pepernoot", 0},
+ {"foo:pepernoot", 0},
+ {"funky", 1},
+ {"fünkÿ", 1},
+ { "", 19 },
+ {"msgid:abcd$efgh@example.com", 1},
+ {"i:abcd$efgh@example.com", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_02(void)
+{
+ const char* q;
+ q = "i:f7ccd24b0808061357t453f5962w8b61f9a453b684d0@mail.gmail.com";
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, q), ==, 1);
+}
+
+static void
+test_mu_query_03(void)
+{
+ int i;
+ QResults queries[] = {{"ploughed", 1},
+ {"i:3BE9E6535E3029448670913581E7A1A20D852173@"
+ "emss35m06.us.lmco.com",
+ 1},
+ {"i:!&!AAAAAAAAAYAAAAAAAAAOH1+8mkk+lLn7Gg5fke7"
+ "FbCgAAAEAAAAJ7eBDgcactKhXL6r8cEnJ8BAAAAAA==@"
+ "example.com",
+ 1},
+
+ /* subsets of the words in the subject should match */
+ {"s:gcc include search order", 1},
+ {"s:gcc include search", 1},
+ {"s:search order", 1},
+ {"s:include", 1},
+
+ {"s:lisp", 1},
+ {"s:LISP", 1},
+
+ // { "s:\"Re: Learning LISP; Scheme vs elisp.\"", 1},
+ // { "subject:Re: Learning LISP; Scheme vs elisp.", 1},
+ // { "subject:\"Re: Learning LISP; Scheme vs elisp.\"", 1},
+ {"to:help-gnu-emacs@gnu.org", 4},
+ //{"t:help-gnu-emacs", 4},
+ {"flag:flagged", 1}};
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_04(void)
+{
+ int i;
+
+ QResults queries[] = {
+ {"frodo@example.com", 1},
+ {"f:frodo@example.com", 1},
+ {"f:Frodo Baggins", 1},
+ {"bilbo@anotherexample.com", 1},
+ {"t:bilbo@anotherexample.com", 1},
+ {"t:bilbo", 1},
+ {"f:bilbo", 0},
+ {"baggins", 1},
+ {"prio:h", 1},
+ {"prio:high", 1},
+ {"prio:normal", 11},
+ {"prio:l", 7},
+ {"not prio:l", 12},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_logic(void)
+{
+ int i;
+ QResults queries[] = {{"subject:gcc", 1},
+ {"subject:lisp", 1},
+ {"subject:gcc OR subject:lisp", 2},
+ {"subject:gcc or subject:lisp", 2},
+ {"subject:gcc AND subject:lisp", 0},
+ {"subject:gcc OR (subject:scheme AND subject:elisp)", 2},
+ {"(subject:gcc OR subject:scheme) AND subject:elisp", 1}};
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_accented_chars_01(void)
+{
+ auto store = Store::make(DB_PATH1);
+ assert_valid_result(store);
+
+ auto qres{store->run_query("fünkÿ")};
+ g_assert_true(!!qres);
+ g_assert_false(qres->empty());
+
+ const auto msg{qres->begin().message()};
+ if (!msg) {
+ g_warning("error getting message");
+ g_assert_not_reached();
+ }
+
+ assert_equal(msg->subject(), "Greetings from Lothlórien");
+}
+
+static void
+test_mu_query_accented_chars_02(void)
+{
+ int i;
+
+ QResults queries[] = {{"f:mü", 1},
+ { "s:motörhead", 1},
+ {"t:Helmut", 1},
+ {"t:Kröger", 1},
+ {"s:MotorHeäD", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ auto count = run_and_count_matches(DB_PATH1, queries[i].query);
+ if (count != queries[i].count)
+ g_warning("query '%s'; expect %zu but got %zu",
+ queries[i].query, queries[i].count, count);
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+ }
+}
+
+static void
+test_mu_query_accented_chars_fraiche(void)
+{
+ int i;
+
+ QResults queries[] = {{"crème fraîche", 1},
+ {"creme fraiche", 1},
+ {"fraîche crème", 1},
+ {"будланула", 1},
+ {"БУДЛАНУЛА", 1},
+ {"CRÈME FRAÎCHE", 1},
+ {"CREME FRAICHE", 1}};
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ if (g_test_verbose())
+ g_print("'%s'\n", queries[i].query);
+
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==,
+ queries[i].count);
+ }
+}
+
+static void
+test_mu_query_wildcards(void)
+{
+ int i;
+
+ QResults queries[] = {
+ {"f:mü", 1},
+ {"s:mo*", 1},
+ {"t:Helm*", 1},
+ {"queensryche", 1},
+ {"Queen*", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_dates_helsinki(void)
+{
+ const auto hki = "Europe/Helsinki";
+ if (!timezone_available(hki)) {
+ g_test_skip("timezone not available");
+ return;
+ }
+
+ int i;
+ const char* old_tz;
+
+ QResults queries[] = {{"date:20080731..20080804", 5},
+ {"date:20080731..20080804 s:gcc", 1},
+ {"date:200808110803..now", 7},
+ {"date:200808110803..today", 7},
+ {"date:200808110801..now", 7}};
+
+ old_tz = set_tz(hki);
+
+ const auto xpath{make_database(MU_TESTMAILDIR)};
+ g_assert_false(xpath.empty());
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+ ==,
+ queries[i].count);
+
+ set_tz(old_tz);
+}
+
+static void
+test_mu_query_dates_sydney(void)
+{
+ const auto syd = "Australia/Sydney";
+ if (!timezone_available(syd)) {
+ g_test_skip("timezone not available");
+ return;
+ }
+
+ int i;
+ const char* old_tz;
+ QResults queries[] = {{"date:20080731..20080804", 5},
+ {"date:20080731..20080804 s:gcc", 1},
+ {"date:200808110803..now", 7},
+ {"date:200808110803..today", 7},
+ {"date:200808110801..now", 7}};
+ old_tz = set_tz(syd);
+
+ const auto xpath{make_database(MU_TESTMAILDIR)};
+ g_assert_false(xpath.empty());
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+ ==,
+ queries[i].count);
+ set_tz(old_tz);
+}
+
+static void
+test_mu_query_dates_la(void)
+{
+ const auto la = "America/Los_Angeles";
+ if (!timezone_available(la)) {
+ g_test_skip("timezone not available");
+ return;
+ }
+
+ int i;
+ const char* old_tz;
+
+ QResults queries[] = {{"date:20080731..20080804", 5},
+ {"date:2008-07-31..2008-08-04", 5},
+ {"date:20080804..20080731", 5},
+ {"date:20080731..20080804 s:gcc", 1},
+ {"date:200808110803..now", 6},
+ {"date:200808110803..today", 6},
+ {"date:200808110801..now", 6}};
+ old_tz = set_tz(la);
+
+ const auto xpath = make_database(MU_TESTMAILDIR);
+ g_assert_false(xpath.empty());
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ /* g_print ("%s\n", queries[i].query); */
+ g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+ ==,
+ queries[i].count);
+ }
+
+ set_tz(old_tz);
+}
+
+static void
+test_mu_query_sizes(void)
+{
+ int i;
+ QResults queries[] = {
+ {"size:0b..2m", 19},
+ {"size:3b..2m", 19},
+ {"size:2k..4k", 4},
+
+ {"size:0b..2m", 19},
+ {"size:2m..0b", 19},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_attach(void)
+{
+ int i;
+ QResults queries[] = {{"j:sittingbull.jpg", 1}, {"file:custer", 0}, {"file:custer.jpg", 1}};
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ if (g_test_verbose())
+ g_print("query: %s\n", queries[i].query);
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==,
+ queries[i].count);
+ }
+}
+
+static void
+test_mu_query_msgid(void)
+{
+ int i;
+ QResults queries[] = {
+ {"i:CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz"
+ "=bfogtmbGGs5A@mail.gmail.com",
+ 1},
+ {"msgid:CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz="
+ "bfogtmbGGs5A@mail.gmail.com",
+ 1},
+
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ if (g_test_verbose())
+ g_print("query: %s\n", queries[i].query);
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==,
+ queries[i].count);
+ }
+}
+
+static void
+test_mu_query_tags(void)
+{
+ int i;
+ QResults queries[] = {
+ {"x:paradise", 1},
+ {"tag:lost", 1},
+ {"tag:lost tag:paradise", 1},
+ {"tag:lost tag:horizon", 0},
+ {"tag:lost OR tag:horizon", 1},
+ {"tag:queensryche", 1},
+ {"tag:Queensrÿche", 1},
+ {"x:paradise,lost", 0},
+ {"x:paradise AND x:lost", 1},
+ {"x:\\\\backslash", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_wom_bat(void)
+{
+ int i;
+ QResults queries[] = {
+ {"maildir:/wom_bat", 3},
+ //{ "\"maildir:/wom bat\"", 3},
+ // as expected, no longer works with new parser
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_signed_encrypted(void)
+{
+ int i;
+ QResults queries[] = {
+ {"flag:encrypted", 2},
+ {"flag:signed", 2},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_multi_to_cc(void)
+{
+ int i;
+ QResults queries[] = {
+ {"to:a@example.com", 1},
+ {"cc:d@example.com", 1},
+ {"to:b@example.com", 1},
+ {"cc:e@example.com", 1},
+ {"cc:e@example.com AND cc:d@example.com", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_tags_02(void)
+{
+ int i;
+ QResults queries[] = {
+ {"x:paradise", 1},
+ {"tag:@NextActions", 1},
+ {"x:queensrÿche", 1},
+ {"tag:lost OR tag:operation*", 2},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==,
+ queries[i].count);
+ }
+}
+
+/* Tests for https://github.com/djcb/mu/issues/380
+
+ On certain platforms, something goes wrong during compilation and the
+ --related option doesn't work.
+*/
+static void
+test_mu_query_threads_compilation_error(void)
+{
+ const auto xpath = make_database(MU_TESTMAILDIR);
+
+ g_assert_cmpuint(run_and_count_matches(xpath, "msgid:uwsireh25.fsf@one.dot.net"), ==, 1);
+
+ g_assert_cmpuint(run_and_count_matches(xpath,
+ "msgid:uwsireh25.fsf@one.dot.net",
+ QueryFlags::IncludeRelated),
+ ==,
+ 3);
+}
+
+/* https://github.com/djcb/mu/issues/1428 */
+static void
+test_mu_query_cjk(void)
+{
+ /* XXX: this doesn't pass yet; return for now */
+ g_test_skip("skip CJK tests");
+ return;
+
+ {
+ g_unsetenv("XAPIAN_CJK_NGRAM");
+ const auto xpath = make_database(MU_TESTMAILDIR_CJK);
+ g_assert_cmpuint(run_and_count_matches(xpath,
+ "サーバがダウンしました",
+ QueryFlags::None),
+ ==, 1);
+ g_assert_cmpuint(run_and_count_matches(xpath,
+ "サーバ",
+ QueryFlags::None),
+ ==, 0);
+ }
+
+ {
+ g_setenv("XAPIAN_CJK_NGRAM", "1", TRUE);
+ const auto xpath = make_database(MU_TESTMAILDIR_CJK);
+ g_assert_cmpuint(run_and_count_matches(xpath,
+ "サーバがダウンしました",
+ QueryFlags::None),
+ ==, 0);
+ g_assert_cmpuint(run_and_count_matches(xpath,
+ "サーバ",
+ QueryFlags::None),
+ ==, 0);
+ }
+}
+
+int
+main(int argc, char* argv[])
+{
+ int rv;
+
+ setlocale(LC_ALL, "");
+
+ mu_test_init(&argc, &argv);
+ DB_PATH1 = make_database(MU_TESTMAILDIR);
+ g_assert_false(DB_PATH1.empty());
+
+ DB_PATH2 = make_database(MU_TESTMAILDIR2);
+ g_assert_false(DB_PATH2.empty());
+
+ g_test_add_func("/mu-query/test-mu-query-01", test_mu_query_01);
+ g_test_add_func("/mu-query/test-mu-query-02", test_mu_query_02);
+ g_test_add_func("/mu-query/test-mu-query-03", test_mu_query_03);
+ g_test_add_func("/mu-query/test-mu-query-04", test_mu_query_04);
+
+ g_test_add_func("/mu-query/test-mu-query-signed-encrypted", test_mu_query_signed_encrypted);
+ g_test_add_func("/mu-query/test-mu-query-multi-to-cc", test_mu_query_multi_to_cc);
+ g_test_add_func("/mu-query/test-mu-query-logic", test_mu_query_logic);
+
+ g_test_add_func("/mu-query/test-mu-query-accented-chars-1",
+ test_mu_query_accented_chars_01);
+ g_test_add_func("/mu-query/test-mu-query-accented-chars-2",
+ test_mu_query_accented_chars_02);
+ g_test_add_func("/mu-query/test-mu-query-accented-chars-fraiche",
+ test_mu_query_accented_chars_fraiche);
+
+ g_test_add_func("/mu-query/test-mu-query-msgid", test_mu_query_msgid);
+
+ g_test_add_func("/mu-query/test-mu-query-wom-bat", test_mu_query_wom_bat);
+
+ g_test_add_func("/mu-query/test-mu-query-wildcards", test_mu_query_wildcards);
+ g_test_add_func("/mu-query/test-mu-query-sizes", test_mu_query_sizes);
+
+ g_test_add_func("/mu-query/test-mu-query-dates-helsinki", test_mu_query_dates_helsinki);
+ g_test_add_func("/mu-query/test-mu-query-dates-sydney", test_mu_query_dates_sydney);
+ g_test_add_func("/mu-query/test-mu-query-dates-la", test_mu_query_dates_la);
+
+ g_test_add_func("/mu-query/test-mu-query-attach", test_mu_query_attach);
+ g_test_add_func("/mu-query/test-mu-query-tags", test_mu_query_tags);
+ g_test_add_func("/mu-query/test-mu-query-tags_02", test_mu_query_tags_02);
+
+ g_test_add_func("/mu-query/test-mu-query-threads-compilation-error",
+ test_mu_query_threads_compilation_error);
+
+ g_test_add_func("/mu-query/test-mu-query-cjk",
+ test_mu_query_cjk);
+ rv = g_test_run();
+
+ return rv;
+}
--- /dev/null
+## Copyright (C) 2008-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+include $(top_srcdir)/gtest.mk
+
+SUBDIRS=
+
+info_TEXINFOS=mu4e.texi
+mu4e_TEXINFOS=fdl.texi
+
+dist_lisp_LISP= \
+ mu4e-actions.el \
+ mu4e-bookmarks.el \
+ mu4e-compose.el \
+ mu4e-contacts.el \
+ mu4e-context.el \
+ mu4e-contrib.el \
+ mu4e-draft.el \
+ mu4e-folders.el \
+ mu4e-headers.el \
+ mu4e-icalendar.el \
+ mu4e-lists.el \
+ mu4e-helpers.el \
+ mu4e-main.el \
+ mu4e-mark.el \
+ mu4e-message.el \
+ mu4e-config.el \
+ mu4e-org.el \
+ mu4e-search.el \
+ mu4e-server.el \
+ mu4e-speedbar.el \
+ mu4e-update.el \
+ mu4e-vars.el \
+ mu4e-view.el \
+ mu4e.el \
+ obsolete/org-mu4e.el
+
+
+EXTRA_DIST= \
+ mu4e-about.org
+
+CLEANFILES= \
+ *.elc
+
+doc_DATA = \
+ mu4e-about.org
+
+##
+## Change as needed.
+##
+BUILDPATH=mu4e
+TEXINFO_CSS_PATH=~/Sources/ext/texinfo-css
+fancyhtml:
+ mkdir -p $(BUILDPATH) ; cp -R $(TEXINFO_CSS_PATH)/static $(BUILDPATH)
+ makeinfo --html --css-ref=static/css/texinfo-klare.css -o $(BUILDPATH) mu4e.texi
--- /dev/null
+* TODO
+
+*** mu4e-get-sub-maildirs
+*** extract mailing list name
+*** mark thread
+*** bounce support
+*** sorting
+*** tool bars
+*** refiling-by-pattern
+*** inspect message (muile)
+*** message statistics
+*** include exchange handling / org integration
+*** integrate bbdb
+*** identity support
+
+
+# Local Variables:
+# mode: org; org-startup-folded: nil
+# End:
--- /dev/null
+@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:
+
--- /dev/null
+## 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.
+
+
+# generate some build data for use in mu4e
+mu4e_meta = configure_file(
+ input: 'mu4e-config.el.in',
+ output: 'mu4e-config.el',
+ install: true,
+ install_dir: mu4e_lispdir,
+ configuration: {
+ 'VERSION' : meson.project_version(),
+ # project_build_root() with meson >= 0.56
+ 'abs_top_builddir': join_paths(meson.current_build_dir()),
+ 'MU_DOC_DIR' : join_paths(datadir, 'doc', 'mu'),
+ })
+
+mu4e_srcs=[
+ 'mu4e-actions.el',
+ 'mu4e-bookmarks.el',
+ 'mu4e-compose.el',
+ 'mu4e-contacts.el',
+ 'mu4e-context.el',
+ 'mu4e-contrib.el',
+ 'mu4e-draft.el',
+ 'mu4e-folders.el',
+ 'mu4e.el',
+ 'mu4e-headers.el',
+ 'mu4e-helpers.el',
+ 'mu4e-icalendar.el',
+ 'mu4e-lists.el',
+ 'mu4e-main.el',
+ 'mu4e-mark.el',
+ 'mu4e-message.el',
+ 'mu4e-org.el',
+ 'mu4e-search.el',
+ 'mu4e-server.el',
+ 'mu4e-speedbar.el',
+ 'mu4e-update.el',
+ 'mu4e-vars.el',
+ 'mu4e-view.el',
+ 'obsolete/org-mu4e.el',
+]
+
+# note, we cannot compile mu4e-config.el without incurring
+# WARNING: Source item
+# '[...]/build/mu4e/mu4e-meta.el' cannot be converted to File object, because
+# it is a generated file. This will become a hard error in the future.
+#
+#... so let's not do that!
+
+# hack-around for native compile issue: copy sources to builddir.
+# see: https://debbugs.gnu.org/db/47/47987.html
+foreach src : mu4e_srcs
+ configure_file(input: src, output:'@BASENAME@.el', copy:true)
+endforeach
+
+foreach src : mu4e_srcs
+ target_name= '@BASENAME@.elc'
+ target_path = join_paths(meson.current_build_dir(), target_name)
+ target_func = '(setq byte-compile-dest-file-function(lambda(_) "' + target_path + '"))'
+
+ custom_target(src.underscorify() + '_el',
+ build_by_default: true,
+ input: src,
+ output: target_name,
+ install_dir: mu4e_lispdir,
+ install: true,
+ command: [emacs,
+ '--no-init-file',
+ '--batch',
+ '--eval', '(setq load-prefer-newer t)',
+ '--eval', target_func,
+ '--directory', meson.current_build_dir(),
+ '--directory', meson.current_source_dir(),
+ '--funcall', 'batch-byte-compile', '@INPUT@'])
+endforeach
+
+# also install the sources and the config
+install_data(mu4e_srcs, install_dir: mu4e_lispdir)
+
+# install mu4e-about.org
+install_data('mu4e-about.org', install_dir : join_paths(datadir, 'doc', 'mu'))
+
+if makeinfo.found()
+
+ fulldate = run_command('date', '+%d %B %Y', check:true).stdout().strip()
+ monthdate = run_command('date', '+%B %Y', check:true).stdout().strip()
+ version_texi_data = configuration_data({
+ 'fulldate' : fulldate,
+ 'monthdate' : monthdate,
+ 'version' : meson.project_version(),
+ })
+ version_texi = configure_file(
+ input: 'version.texi.in',
+ output: 'version.texi',
+ configuration: version_texi_data)
+
+ custom_target('mu4e_info',
+ input: 'mu4e.texi',
+ output: 'mu4e.info',
+ install_dir: infodir,
+ install: true,
+ command: [makeinfo,
+ '-o', join_paths(meson.current_build_dir(), 'mu4e.info'),
+ join_paths(meson.current_source_dir(), 'mu4e.texi'),
+ '-I', join_paths(meson.current_build_dir(), '..')])
+ if install_info.found()
+ meson.add_install_script(install_info_script, 'share/info', 'mu4e.info')
+ endif
+endif
--- /dev/null
+#+STARTUP:showall
+* About mu4e
+
+ *mu4e* is an emacs e-mail client based on the [[http://djcbsoftware.nl/code/mu][mu]] email search engine. It was
+ written & designed by /Dirk-Jan C. Binnema/, with contributions from others.
+
+ *mu4e* and *mu* are free software, licensed under the terms of the [[http://www.gnu.org/licenses/gpl-3.0.html][GNU GPLv3]].
+
+ You can get the code from [[https://github.com/djcb/mu][the git repository]]; there, you can also
+ [[https://github.com/djcb/mu/issues][file bugs and feature requests]].
+
+ *mu4e* has its own [[info:mu4e][manual]], which includes an [[info:mu4e#FAQ%20-%20Frequently%20Anticipated%20Questions][FAQ]]. If that is not enough,
+ there's also the [[http://groups.google.com/group/mu-discuss][mu mailing list]].
+
+ [Press *q* to quit this buffer]
--- /dev/null
+;;; mu4e-actions.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Example actions for messages, attachments (see chapter 'Actions' in the
+;; manual)
+
+;;; Code:
+
+(require 'ido)
+
+(require 'mu4e-helpers)
+(require 'mu4e-message)
+(require 'mu4e-search)
+(require 'mu4e-contacts)
+\f
+;;; Count lines
+
+(defun mu4e-action-count-lines (msg)
+ "Count the number of lines in the e-mail MSG.
+Works for headers view and message-view."
+ (message "Number of lines: %s"
+ (shell-command-to-string
+ (concat "wc -l < "
+ (shell-quote-argument (mu4e-message-field msg :path))))))
+
+;;; Org Helpers
+
+(defvar mu4e-captured-message nil
+ "The most recently captured message.")
+
+(defun mu4e-action-capture-message (msg)
+ "Remember MSG.
+Later, we can create an attachment based on this message with
+`mu4e-compose-attach-captured-message'."
+ (setq mu4e-captured-message msg)
+ (message "Message has been captured"))
+
+
+(defun mu4e-action-copy-message-file-path (msg)
+ "Save the full path for the current MSG to the kill ring."
+ (kill-new (mu4e-message-field msg :path)))
+
+(defvar mu4e-org-contacts-file nil
+ "File to store contact information for org-contacts.
+Needed by `mu4e-action-add-org-contact'.")
+
+(eval-when-compile ;; silence compiler warning about free variable
+ (unless (require 'org-capture nil 'noerror)
+ (defvar org-capture-templates nil)))
+
+(defun mu4e-action-add-org-contact (msg)
+ "Add an org-contact based on the sender ddress of the current MSG.
+You need to set `mu4e-org-contacts-file' to the full path to the
+file where you store your org-contacts."
+ (unless (require 'org-capture nil 'noerror)
+ (mu4e-error "Feature org-capture is not available"))
+ (unless mu4e-org-contacts-file
+ (mu4e-error "Variable `mu4e-org-contacts-file' is nil"))
+ (let* ((sender (car-safe (mu4e-message-field msg :from)))
+ (name (mu4e-contact-name sender))
+ (email (mu4e-contact-email sender))
+ (blurb
+ (format
+ (concat
+ "* %%?%s\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: %s\n"
+ ":NICK:\n"
+ ":BIRTHDAY:\n"
+ ":END:\n\n")
+ (or name email "")
+ (or email "")))
+ (key "mu4e-add-org-contact-key")
+ (org-capture-templates
+ (append org-capture-templates
+ (list (list key "contacts" 'entry
+ (list 'file mu4e-org-contacts-file) blurb)))))
+ (when (fboundp 'org-capture)
+ (org-capture nil key))))
+
+;;; Patches
+
+(defvar mu4e--patch-directory-history nil
+ "History of directories we have applied patches to.")
+
+;; This essentially works around the fact that read-directory-name
+;; can't have custom history.
+(defun mu4e--read-patch-directory (&optional prompt)
+ "Read a `PROMPT'ed directory name via `completing-read' with history."
+ (unless prompt
+ (setq prompt "Target directory:"))
+ (file-truename
+ (completing-read prompt 'read-file-name-internal #'file-directory-p
+ nil nil 'mu4e--patch-directory-history)))
+
+(defun mu4e-action-git-apply-patch (msg)
+ "Apply `MSG' as a git patch."
+ (let ((path (mu4e--read-patch-directory "Target directory: ")))
+ (let ((default-directory path))
+ (shell-command
+ (format "git apply %s"
+ (shell-quote-argument (mu4e-message-field msg :path)))))))
+
+(defun mu4e-action-git-apply-mbox (msg &optional signoff)
+ "Apply `MSG' a git patch with optional `SIGNOFF'.
+
+If the `default-directory' matches the most recent history entry don't
+bother asking for the git tree again (useful for bulk actions)."
+
+ (let ((cwd (substring-no-properties
+ (or (car mu4e--patch-directory-history)
+ "not-a-dir"))))
+ (unless (and (stringp cwd) (string= default-directory cwd))
+ (setq cwd (mu4e--read-patch-directory "Target directory: ")))
+ (let ((default-directory cwd))
+ (shell-command
+ (format "git am %s %s"
+ (if signoff "--signoff" "")
+ (shell-quote-argument (mu4e-message-field msg :path)))))))
+
+;;; Tagging
+
+(defvar mu4e-action-tags-header "X-Keywords"
+ "Header where tags are stored.
+Used by `mu4e-action-retag-message'. Make sure it is one of the
+headers mu recognizes for storing tags: X-Keywords, X-Label,
+Keywords. Also note that changing this setting on already tagged
+messages can lead to messages with multiple tags headers.")
+
+(defvar mu4e-action-tags-completion-list '()
+ "List of tags for completion in `mu4e-action-retag-message'.")
+
+(defun mu4e--contains-line-matching (regexp path)
+ "Return non-nil if the file at PATH contain a line matching REGEXP.
+Otherwise return nil."
+ (with-temp-buffer
+ (insert-file-contents path)
+ (save-excursion
+ (goto-char (point-min))
+ (re-search-forward regexp nil t))))
+
+(defun mu4e--replace-first-line-matching (regexp to-string path)
+ "Replace first line matching REGEXP in PATH with TO-STRING."
+ (with-temp-file path
+ (insert-file-contents path)
+ (save-excursion
+ (goto-char (point-min))
+ (if (re-search-forward regexp nil t)
+ (replace-match to-string nil nil)))))
+
+(declare-function mu4e--server-add "mu4e-server")
+(defun mu4e--refresh-message (path)
+ "Re-parse message at PATH.
+if this works, we will
+receive (:info add :path <path> :docid <docid>) as well as (:update
+<msg-sexp>)."
+ (mu4e--server-add path))
+
+(defun mu4e-action-retag-message (msg &optional retag-arg)
+ "Change tags of MSG with RETAG-ARG.
+
+RETAG-ARG is a comma-separated list of additions and removals.
+
+Example: +tag,+long tag,-oldtag
+would add \"tag\" and \"long tag\", and remove \"oldtag\"."
+ (let* (
+ (path (mu4e-message-field msg :path))
+ (oldtags (mu4e-message-field msg :tags))
+ (tags-completion
+ (append
+ mu4e-action-tags-completion-list
+ (mapcar (lambda (tag) (format "+%s" tag))
+ mu4e-action-tags-completion-list)
+ (mapcar (lambda (tag) (format "-%s" tag))
+ oldtags)))
+ (retag (if retag-arg
+ (split-string retag-arg ",")
+ (completing-read-multiple "Tags: " tags-completion)))
+ (header mu4e-action-tags-header)
+ (sep (cond ((string= header "Keywords") ", ")
+ ((string= header "X-Label") " ")
+ ((string= header "X-Keywords") ", ")
+ (t ", ")))
+ (taglist (if oldtags (copy-sequence oldtags) '()))
+ tagstr)
+ (dolist (tag retag taglist)
+ (cond
+ ((string-match "^\\+\\(.+\\)" tag)
+ (setq taglist (push (match-string 1 tag) taglist)))
+ ((string-match "^\\-\\(.+\\)" tag)
+ (setq taglist (delete (match-string 1 tag) taglist)))
+ (t
+ (setq taglist (push tag taglist)))))
+
+ (setq taglist (sort (delete-dups taglist) 'string<))
+ (setq tagstr (mapconcat 'identity taglist sep))
+
+ (setq tagstr (replace-regexp-in-string "[\\&]" "\\\\\\&" tagstr))
+ (setq tagstr (replace-regexp-in-string "[/]" "\\&" tagstr))
+
+ (if (not (mu4e--contains-line-matching (concat header ":.*") path))
+ ;; Add tags header just before the content
+ (mu4e--replace-first-line-matching
+ "^$" (concat header ": " tagstr "\n") path)
+
+ ;; replaces keywords, restricted to the header
+ (mu4e--replace-first-line-matching
+ (concat header ":.*")
+ (concat header ": " tagstr)
+ path))
+
+ (mu4e-message (concat "tagging: " (mapconcat 'identity taglist ", ")))
+ (mu4e--refresh-message path)))
+
+(defun mu4e-action-show-thread (msg)
+ "Show thread for message at point with point remaining on MSG.
+I.e., point remains on the message with the message-id where the
+action was invoked. If invoked in view mode, continue to display
+the message."
+ (let ((msgid (mu4e-message-field msg :message-id)))
+ (when msgid
+ (let ((mu4e-search-threads t)
+ (mu4e-headers-include-related t))
+ (mu4e-search
+ (format "msgid:%s" msgid)
+ nil nil nil
+ msgid (and (eq major-mode 'mu4e-view-mode)
+ (not (eq mu4e-split-view 'single-window))))))))
+;;; _
+(provide 'mu4e-actions)
+;;; mu4e-actions.el ends here
--- /dev/null
+;;; mu4e-bookmarks.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(require 'mu4e-helpers)
+
+\f
+;;; Configuration
+
+(defgroup mu4e-bookmarks nil
+ "Settings for bookmarks."
+ :group 'mu4e)
+
+(defcustom mu4e-bookmarks
+ '(( :name "Unread messages"
+ :query "flag:unread AND NOT flag:trashed"
+ :key ?u)
+ ( :name "Today's messages"
+ :query "date:today..now"
+ :key ?t)
+ ( :name "Last 7 days"
+ :query "date:7d..now"
+ :hide-unread t
+ :key ?w)
+ ( :name "Messages with images"
+ :query "mime:image/*"
+ :key ?p))
+ "List of pre-defined queries that are shown on the main screen.
+
+Each of the list elements is a plist with at least:
+`:name' - the name of the query
+`:query' - the query expression or function
+`:key' - the shortcut key.
+
+Note that the :query parameter can be a function/lambda.
+
+Optionally, you can add the following: `:hide' - if t, the
+bookmark is hidden from the main-view and speedbar.
+`:hide-unread' - do not show the counts of unread/total number of
+matches for the query in the main-view. This can be useful if a
+bookmark uses a very slow query.
+
+`:hide-unread' is implied from `:hide'. Furthermore, it is
+implied when `:query' is a function.
+
+Note: for efficiency, queries used to determine the unread/all
+counts do not discard duplicate or unreadable messages. Thus, the
+numbers shown may differ from the number you get from a normal
+query."
+ :type '(repeat (plist))
+ :group 'mu4e-bookmarks)
+
+\f
+ (defun mu4e-ask-bookmark (prompt)
+ "Ask the user for a bookmark (using PROMPT) as defined in
+`mu4e-bookmarks', then return the corresponding query."
+ (unless (mu4e-bookmarks) (mu4e-error "No bookmarks defined"))
+ (let* ((prompt (mu4e-format "%s" prompt))
+ (bmarks
+ (mapconcat
+ (lambda (bm)
+ (concat
+ "[" (propertize (make-string 1 (plist-get bm :key))
+ 'face 'mu4e-highlight-face)
+ "]"
+ (plist-get bm :name))) (mu4e-bookmarks) ", "))
+ (kar (read-char (concat prompt bmarks))))
+ (mu4e-get-bookmark-query kar)))
+
+(defun mu4e-get-bookmark-query (kar)
+ "Get the corresponding bookmarked query for shortcut KAR.
+Raise an error if none is found."
+ (let* ((chosen-bm
+ (or (seq-find
+ (lambda (bm)
+ (= kar (plist-get bm :key)))
+ (mu4e-bookmarks))
+ (mu4e-warn "Unknown shortcut '%c'" kar)))
+ (expr (plist-get chosen-bm :query))
+ (expr (if (not (functionp expr)) expr
+ (funcall expr)))
+ (query (eval expr)))
+ (if (stringp query)
+ query
+ (mu4e-warn "Expression must evaluate to query string ('%S')" expr))))
+
+
+(defun mu4e-bookmark-define (query name key)
+ "Define a bookmark for QUERY with NAME and shortcut KEY.
+Append it to `mu4e-bookmarks'. Replaces any existing bookmark
+with KEY."
+ (setq mu4e-bookmarks
+ (seq-remove
+ (lambda (bm)
+ (= (plist-get bm :key) key))
+ (mu4e-bookmarks)))
+ (cl-pushnew `(:name ,name
+ :query ,query
+ :key ,key)
+ mu4e-bookmarks :test 'equal))
+
+(defun mu4e-bookmarks ()
+ "Get `mu4e-bookmarks' in the (new) format.
+Convert from the old format if needed."
+ (seq-map (lambda (item)
+ (if (and (listp item) (= (length item) 3))
+ `(:name ,(nth 1 item) :query ,(nth 0 item)
+ :key ,(nth 2 item))
+ item)) mu4e-bookmarks))
+\f
+(provide 'mu4e-bookmarks)
+;;; mu4e-bookmarks.el ends here
--- /dev/null
+;;; mu4e-compose.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file, various functions to compose/send messages, piggybacking on
+;; gnus' message mode
+
+;; Magic / Rupe Goldberg
+
+;; 1) When we reply/forward a message, we get it from the backend, ie:
+;; we send to the backend (mu4e-compose):
+;; compose type:reply docid:30935
+;; backend responds with:
+;; (:compose reply :original ( .... <original message> ))
+
+;; 2) When we compose a message, message and headers are separated by
+;; `mail-header-separator', ie. '--text follows this line--. We use
+;; before-save-hook and after-save-hook to remove/re-add this special line, so
+;; it stays in the buffer, but never hits the disk.
+;; see:
+;; mu4e~compose-insert-mail-header-separator
+;; mu4e~compose-remove-mail-header-separator
+;;
+;; (maybe we can get away with remove it only just before sending? what does
+;; gnus do?)
+
+;; 3) When sending a message, we want to do a few things:
+;; a) move the message from drafts to the sent folder (maybe; depends on
+;; `mu4e-sent-messages-behavior')
+;; b) if it's a reply, mark the replied-to message as "R", i.e. replied
+;; if it's a forward, mark the forwarded message as "P", i.e.
+;; passed (forwarded)
+;; c) kill all buffers looking at the sent message
+
+;; a) is dealt with by message-mode, but we need to tell it where to move the
+;; sent message. We do this by adding an Fcc: header with the target folder,
+;; see `mu4e~compose-setup-fcc-maybe'. Since message-mode does not natively
+;; understand maildirs, we also need to tell it what to do, so we also set
+;; `message-fcc-handler-function' there. Finally, we add the the message in
+;; the sent-folder to the database.
+;;
+;; b) this is handled in `mu4e~compose-set-parent-flag'
+;;
+;; c) this is handled in our handler for the `sent'-message from the backend
+;; (`mu4e-sent-handler')
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'message)
+(require 'mail-parse)
+(require 'smtpmail)
+
+(require 'mu4e-server)
+(require 'mu4e-actions)
+(require 'mu4e-message)
+(require 'mu4e-draft)
+(require 'mu4e-context)
+
+\f
+;;; Configuration
+;; see mu4e-drafts.el
+\f
+;;; Attachments
+(defun mu4e-compose-attach-message (msg)
+ "Insert message MSG as an attachment."
+ (let ((path (plist-get msg :path)))
+ (unless (file-exists-p path)
+ (mu4e-warn "Message file not found"))
+ (mml-attach-file
+ path
+ "message/rfc822"
+ (or (plist-get msg :subject) "No subject")
+ "attachment")))
+
+(defun mu4e-compose-attach-captured-message ()
+ "Insert the last captured message file as an attachment.
+Messages are captured with `mu4e-action-capture-message'."
+ (interactive)
+ (unless mu4e-captured-message
+ (mu4e-warn "No message has been captured"))
+ (mu4e-compose-attach-message mu4e-captured-message))
+
+;;; Misc
+
+;; 'fcc' refers to saving a copy of a sent message to a certain folder. that's
+
+;; what these 'Sent mail' folders are for!
+;;
+;; We let message mode take care of this by adding a field
+
+;; Fcc: <full-path-to-message-in-target-folder>
+
+;; in the "message-send-hook" (ie., just before sending). message mode will
+;; then take care of the saving when the message is actually sent.
+;;
+;; note, where and if you make this copy depends on the value of
+;; `mu4e-sent-messages-behavior'.
+
+(defun mu4e~compose-setup-fcc-maybe ()
+ "Maybe setup Fcc, based on `mu4e-sent-messages-behavior'.
+If needed, set the Fcc header, and register the handler function."
+ (let* ((sent-behavior
+ ;; Note; we cannot simply use functionp here, since at least
+ ;; delete is a function, too...
+ (if (member mu4e-sent-messages-behavior '(delete trash sent))
+ mu4e-sent-messages-behavior
+ (if (functionp mu4e-sent-messages-behavior)
+ (funcall mu4e-sent-messages-behavior)
+ mu4e-sent-messages-behavior)))
+ (mdir
+ (pcase sent-behavior
+ ('delete nil)
+ ('trash (mu4e-get-trash-folder mu4e-compose-parent-message))
+ ('sent (mu4e-get-sent-folder mu4e-compose-parent-message))
+ (_ (mu4e-error
+ "Unsupported value %S for `mu4e-sent-messages-behavior'"
+ mu4e-sent-messages-behavior))))
+ (fccfile (and mdir
+ (concat (mu4e-root-maildir) mdir "/cur/"
+ (mu4e~draft-message-filename-construct "S")))))
+ ;; if there's an fcc header, add it to the file
+ (when fccfile
+ (message-add-header (concat "Fcc: " fccfile "\n"))
+ ;; sadly, we cannot define as 'buffer-local'... this will screw up gnus
+ ;; etc. if you run it after mu4e so, (hack hack) we reset it to the old
+ ;; handler after we've done our thing.
+ (setq message-fcc-handler-function
+ (let ((maildir mdir)
+ (old-handler message-fcc-handler-function))
+ (lambda (file)
+ (setq message-fcc-handler-function old-handler)
+ ;; reset the fcc handler
+ (let ((mdir-path (concat (mu4e-root-maildir) maildir)))
+ ;; Create the full maildir structure for the sent folder if it
+ ;; doesn't exist. `mu4e--server-mkdir` runs asynchronously but
+ ;; no matter whether it runs before or after `write-file`, the
+ ;; sent maildir ends up in the correct state.
+ (unless (file-exists-p mdir-path)
+ (mu4e--server-mkdir mdir-path)))
+ (write-file file) ;; writing maildirs files is easy
+ (mu4e--server-add file))))))) ;; update the database
+
+(defvar mu4e-compose-hidden-headers
+ `("^References:" "^Face:" "^X-Face:"
+ "^X-Draft-From:" "^User-agent:")
+ "Hidden headers when composing.")
+
+(defun mu4e~compose-hide-headers ()
+ "Hide the headers as per `mu4e-compose-hidden-headers'."
+ (let ((message-hidden-headers mu4e-compose-hidden-headers))
+ (message-hide-headers)))
+
+(defconst mu4e~compose-address-fields-regexp
+ "^\\(To\\|B?Cc\\|Reply-To\\|From\\):")
+
+(defun mu4e~compose-register-message-save-hooks ()
+ "Just before saving, we remove the `mail-header-separator'.
+Just after saving we restore it; thus, the separator should never
+appear on disk. Also update the Date and ensure we have a
+Message-ID."
+ (add-hook 'before-save-hook
+ #'mu4e~compose-before-save-hook-fn
+ nil t)
+ (add-hook 'after-save-hook
+ #'mu4e~compose-after-save-hook-fn
+ nil t))
+
+(defvar-local mu4e~compose-undo nil
+ "Remember the undo-state.")
+
+(defun mu4e~compose-before-save-hook-fn ()
+ "Add the message-id if necessary and update the date."
+ (setq mu4e~compose-undo buffer-undo-list)
+ (save-excursion
+ (save-restriction
+ (message-narrow-to-headers)
+ (unless (message-fetch-field "Message-ID")
+ (message-generate-headers '(Message-ID)))
+ (message-generate-headers '(Date)))
+ (save-match-data
+ (mu4e~draft-remove-mail-header-separator))))
+
+(defun mu4e~compose-after-save-hook-fn ()
+ (save-match-data
+ (mu4e~compose-set-friendly-buffer-name)
+ (mu4e~draft-insert-mail-header-separator)
+ ;; hide some headers again
+ (widen)
+ (mu4e~compose-hide-headers)
+ (set-buffer-modified-p nil)
+ (mu4e-message "Saved (%d lines)" (count-lines (point-min) (point-max)))
+ ;; update the file on disk -- ie., without the separator
+ (mu4e--server-add (buffer-file-name)))
+ (setq buffer-undo-list mu4e~compose-undo))
+
+\f
+;;; address completion
+
+;; inspired by org-contacts.el and
+;; https://github.com/nordlow/elisp/blob/master/mine/completion-styles-cycle.el
+
+(defun mu4e~compose-complete-handler (str pred action)
+ "Complete address STR with predication PRED for ACTION."
+ (cond
+ ((eq action nil)
+ (try-completion str mu4e--contacts-set pred))
+ ((eq action t)
+ (all-completions str mu4e--contacts-set pred))
+ ((eq action 'metadata)
+ ;; our contacts are already sorted - just need to tell the
+ ;; completion machinery not to try to undo that...
+ '(metadata
+ (display-sort-function . identity)
+ (cycle-sort-function . identity)))))
+
+(defun mu4e~compose-complete-contact (&optional start)
+ "Complete the text at START with a contact.
+Ie. either \"name <email>\" or \"email\")."
+ (interactive)
+ (let ((mail-abbrev-mode-regexp mu4e~compose-address-fields-regexp)
+ (eoh ;; end-of-headers
+ (save-excursion
+ (goto-char (point-min))
+ (search-forward-regexp mail-header-separator nil t))))
+ ;; try to complete only when we're in the headers area,
+ ;; looking at an address field.
+ (when (and eoh (> eoh (point)) (mail-abbrev-in-expansion-header-p))
+ (let* ((end (point))
+ (start
+ (or start
+ (save-excursion
+ (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
+ (goto-char (match-end 0))
+ (point)))))
+ (list start end 'mu4e~compose-complete-handler)))))
+
+(defun mu4e~compose-setup-completion ()
+ "Set up auto-completion of addresses."
+ (set (make-local-variable 'completion-ignore-case) t)
+ (set (make-local-variable 'completion-cycle-threshold) 7)
+ (add-to-list (make-local-variable 'completion-styles) 'substring)
+ (add-hook 'completion-at-point-functions
+ 'mu4e~compose-complete-contact nil t))
+
+(defun mu4e~remove-refs-maybe ()
+ "Remove References: if In-Reply-To: is missing.
+This allows the user to effectively start a new message-thread by
+removing the In-Reply-To header."
+ (unless (message-fetch-field "in-reply-to")
+ (message-remove-header "References")))
+
+;;; Compose Mode
+
+(defvar mu4e-compose-mode-map nil
+ "Keymap for \"*mu4e-compose*\" buffers.")
+(unless mu4e-compose-mode-map
+ (setq mu4e-compose-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-S-u") 'mu4e-update-mail-and-index)
+ (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index)
+ (define-key map (kbd "C-c C-k") 'mu4e-message-kill-buffer)
+ (define-key map (kbd "C-c ;") 'mu4e-compose-context-switch)
+ (define-key map (kbd "M-q") 'mu4e-fill-paragraph)
+ map)))
+
+(defun mu4e-fill-paragraph (&optional region)
+ "Re-layout either the whole message or REGION.
+If variable `use-hard-newlines', takes a multi-line paragraph and
+makes it into a single line of text. Assume paragraphs are
+separated by blank lines. If variable `use-hard-newlines' is not
+set, this simply executes `fill-paragraph'."
+ ;; Inspired by https://www.emacswiki.org/emacs/UnfillParagraph
+ (interactive (progn (barf-if-buffer-read-only) '(t)))
+ (ignore-errors
+ (if mu4e-compose-format-flowed
+ (let ((fill-column (point-max))
+ (use-hard-newlines nil)); rfill "across" hard newlines
+ (when (use-region-p)
+ (delete-trailing-whitespace (region-beginning) (region-end)))
+ (fill-paragraph nil region))
+ (when (use-region-p)
+ (delete-trailing-whitespace (region-beginning) (region-end)))
+ (fill-paragraph nil region))))
+
+(defun mu4e-toggle-use-hard-newlines ()
+ (interactive)
+ (setq use-hard-newlines (not use-hard-newlines))
+ (if use-hard-newlines
+ (turn-off-auto-fill)
+ (turn-on-auto-fill)))
+
+(defun mu4e~compose-remap-faces ()
+ "Remap `message-mode' faces to mu4e ones.
+Our parent `message-mode' uses font-locking for the compose
+buffers; lets remap its faces so it uses the ones for mu4e."
+ ;; normal headers
+ (face-remap-add-relative 'message-header-name
+ '((:inherit mu4e-header-key-face)))
+ (face-remap-add-relative 'message-header-other
+ '((:inherit mu4e-header-value-face)))
+ ;; special headers
+ (face-remap-add-relative 'message-header-from
+ '((:inherit mu4e-contact-face)))
+ (face-remap-add-relative 'message-header-to
+ '((:inherit mu4e-contact-face)))
+ (face-remap-add-relative 'message-header-cc
+ '((:inherit mu4e-contact-face)))
+ (face-remap-add-relative 'message-header-bcc
+ '((:inherit mu4e-contact-face)))
+ (face-remap-add-relative 'message-header-subject
+ '((:inherit mu4e-special-header-value-face)))
+ ;; citation
+ (face-remap-add-relative 'message-cited-text
+ '((:inherit mu4e-cited-1-face))))
+
+(define-derived-mode mu4e-compose-mode message-mode "mu4e:compose"
+ "Major mode for the mu4e message composition, derived from `message-mode'.
+\\{message-mode-map}."
+ (progn
+ (use-local-map mu4e-compose-mode-map)
+
+ (mu4e-context-minor-mode)
+ (define-key mu4e-context-minor-mode-map (kbd ";") nil)
+ (define-key mu4e-context-minor-mode-map (kbd "C-c C-;")
+ #'mu4e-compose-context-switch)
+
+ (set (make-local-variable 'message-signature) mu4e-compose-signature)
+ ;; set this to allow mu4e to work when gnus-agent is unplugged in gnus
+ (set (make-local-variable 'message-send-mail-real-function) nil)
+ ;; Set to nil to enable `electric-quote-local-mode' to work:
+ (make-local-variable 'comment-use-syntax)
+ (setq comment-use-syntax nil)
+ ;; message-mode has font-locking, but uses its own faces. Let's
+ ;; use the mu4e-specific ones instead
+ (mu4e~compose-remap-faces)
+ (mu4e~compose-register-message-save-hooks)
+ ;; offer completion for e-mail addresses
+ (when mu4e-compose-complete-addresses
+ (unless mu4e--contacts-set
+ ;; work-around for https://github.com/djcb/mu/issues/1016
+ (mu4e--request-contacts-maybe))
+ (mu4e~compose-setup-completion))
+ (if mu4e-compose-format-flowed
+ (progn
+ (turn-off-auto-fill)
+ (setq truncate-lines nil
+ word-wrap t
+ mml-enable-flowed t
+ use-hard-newlines t)
+ (visual-line-mode t))
+ (setq mml-enable-flowed nil))
+
+ ;; set the attachment dir to something more reasonable than the draft
+ ;; directory.
+ (setq default-directory (mu4e~get-attachment-dir))
+
+ (let ((keymap (lookup-key message-mode-map [menu-bar text])))
+ (when keymap
+ (define-key-after
+ keymap
+ [mu4e-hard-newlines]
+ '(menu-item "Format=flowed" mu4e-toggle-use-hard-newlines
+ :button (:toggle . use-hard-newlines)
+ :help "Toggle format=flowed"
+ :visible (eq major-mode 'mu4e-compose-mode)
+ :enable mu4e-compose-format-flowed)
+ 'sep)
+
+ (define-key-after
+ keymap
+ [mu4e-electric-quote-mode]
+ '(menu-item "Electric quote" electric-quote-local-mode
+ :button (:toggle . electric-quote-mode)
+ :help "Toggle Electric quote mode"
+ :visible (and (eq major-mode 'mu4e-compose-mode)
+ (functionp 'electric-quote-local-mode)))
+ 'mu4e-hard-newlines)))
+
+ (when (lookup-key mml-mode-map [menu-bar Attachments])
+ (define-key-after
+ (lookup-key mml-mode-map [menu-bar Attachments])
+ [mu4e-compose-attach-captured-message]
+ '(menu-item "Attach captured message"
+ mu4e-compose-attach-captured-message
+ :help "Attach message captured in Headers View (with 'a c')"
+ :visible (eq major-mode 'mu4e-compose-mode))
+ (quote Attach\ External...)))
+
+ ;; setup the fcc-stuff, if needed
+ (add-hook 'message-send-hook
+ #'mu4e~setup-fcc-message-sent-hook-fn
+ nil t)
+ ;; when the message has been sent.
+ (add-hook 'message-sent-hook
+ #'mu4e~set-sent-handler-message-sent-hook-fn
+ nil t))
+ ;; mark these two hooks as permanent-local, so they'll survive mode-changes
+ ;; (put 'mu4e~compose-save-before-sending 'permanent-local-hook t)
+ (put 'mu4e~compose-mark-after-sending 'permanent-local-hook t))
+
+(defun mu4e~setup-fcc-message-sent-hook-fn ()
+ ;; mu4e~compose-save-before-sending
+ ;; when in-reply-to was removed, remove references as well.
+ (when (eq mu4e-compose-type 'reply)
+ (mu4e~remove-refs-maybe))
+ (when use-hard-newlines
+ (mu4e-send-harden-newlines))
+ ;; for safety, always save the draft before sending
+ (set-buffer-modified-p t)
+ (save-buffer)
+ (mu4e~compose-setup-fcc-maybe)
+ (widen))
+
+(defun mu4e~set-sent-handler-message-sent-hook-fn ()
+ ;; mu4e~compose-mark-after-sending
+ (setq mu4e-sent-func 'mu4e-sent-handler)
+ (mu4e--server-sent (buffer-file-name)))
+
+(defun mu4e-send-harden-newlines ()
+ "Set the hard property to all newlines."
+ (save-excursion
+ (goto-char (point-min))
+ (while (search-forward "\n" nil t)
+ (put-text-property (1- (point)) (point) 'hard t))))
+
+(defconst mu4e~compose-buffer-max-name-length 30
+ "Maximum length of the mu4e-send-buffer-name.")
+
+(defun mu4e~compose-set-friendly-buffer-name (&optional compose-type)
+ "Set some user-friendly buffer name based on the COMPOSE-TYPE."
+ (let* ((subj (message-field-value "subject"))
+ (subj (unless (and subj (string-match "^[:blank:]*$" subj)) subj))
+ (str (or subj
+ (pcase compose-type
+ ('reply "*reply*")
+ ('forward "*forward*")
+ (_ "*draft*")))))
+ (rename-buffer (generate-new-buffer-name
+ (truncate-string-to-width
+ str mu4e~compose-buffer-max-name-length)
+ (buffer-name)))))
+
+(defun mu4e-compose-crypto-message (parent compose-type)
+ "Possibly encrypt or sign a message based on PARENT and COMPOSE-TYPE.
+See `mu4e-compose-crypto-policy' for more details."
+ (let* ((encrypted-p
+ (and parent (memq 'encrypted (mu4e-message-field parent :flags))))
+ (encrypt
+ (or (memq 'encrypt-all-messages mu4e-compose-crypto-policy)
+ ;; new messages
+ (and (memq 'encrypt-new-messages mu4e-compose-crypto-policy)
+ (eq compose-type 'new))
+ ;; forwarded messages
+ (and (eq compose-type 'forward)
+ (memq 'encrypt-forwarded-messages mu4e-compose-crypto-policy))
+ ;; edited messages
+ (and (eq compose-type 'edit)
+ (memq 'encrypt-edited-messages mu4e-compose-crypto-policy))
+ ;; all replies
+ (and (eq compose-type 'reply)
+ (memq 'encrypt-all-replies mu4e-compose-crypto-policy))
+ ;; plain replies
+ (and (eq compose-type 'reply) (not encrypted-p)
+ (memq 'encrypt-plain-replies mu4e-compose-crypto-policy))
+ ;; encrypted replies
+ (and (eq compose-type 'reply) encrypted-p
+ (memq 'encrypt-encrypted-replies
+ mu4e-compose-crypto-policy))))
+ (sign
+ (or (memq 'sign-all-messages mu4e-compose-crypto-policy)
+ ;; new messages
+ (and (eq compose-type 'new)
+ (memq 'sign-new-messages mu4e-compose-crypto-policy))
+ ;; forwarded messages
+ (and (eq compose-type 'forward)
+ (memq 'sign-forwarded-messages mu4e-compose-crypto-policy))
+ ;; edited messages
+ (and (eq compose-type 'edit)
+ (memq 'sign-edited-messages mu4e-compose-crypto-policy))
+ ;; all replies
+ (and (eq compose-type 'reply)
+ (memq 'sign-all-replies mu4e-compose-crypto-policy))
+ ;; plain replies
+ (and (eq compose-type 'reply) (not encrypted-p)
+ (memq 'sign-plain-replies mu4e-compose-crypto-policy))
+ ;; encrypted replies
+ (and (eq compose-type 'reply) encrypted-p
+ (memq 'sign-encrypted-replies mu4e-compose-crypto-policy)))))
+ (cond ((and sign encrypt)
+ (mml-secure-message-sign-encrypt))
+ (sign (mml-secure-message-sign))
+ (encrypt (mml-secure-message-encrypt)))))
+
+(cl-defun mu4e~compose-handler (compose-type &optional original-msg includes
+ switch-function)
+ "Create a new draft message, or open an existing one.
+
+COMPOSE-TYPE determines the kind of message to compose and is a
+symbol, either `reply', `forward', `edit', `resend' `new'. `edit'
+is for editing existing (draft) messages. When COMPOSE-TYPE is
+`reply' or `forward', MSG should be a message plist. If
+COMPOSE-TYPE is `new', ORIGINAL-MSG should be nil.
+
+Optionally (when forwarding, replying) ORIGINAL-MSG is the original
+message we will forward / reply to.
+
+Optionally (when inline forwarding) INCLUDES contains a list of
+ (:file-name <filename> :mime-type <mime-type>
+ :description <description> :disposition <disposition>)
+or
+ (:buffer-name <filename> :mime-type <mime-type>
+ :description <description> :disposition <disposition>)
+for the attachments to include; file-name refers to
+a file which our backend has conveniently saved for us (as a
+tempfile). The properties :mime-type, :description and :disposition
+are optional."
+
+ ;; Run the hooks defined for `mu4e-compose-pre-hook'. If compose-type is
+ ;; `reply', `forward' or `edit', `mu4e-compose-parent-message' points to the
+ ;; message being forwarded or replied to, otherwise it is nil.
+ (set (make-local-variable 'mu4e-compose-parent-message) original-msg)
+ (put 'mu4e-compose-parent-message 'permanent-local t)
+ ;; remember the compose-type
+ (set (make-local-variable 'mu4e-compose-type) compose-type)
+ (put 'mu4e-compose-type 'permanent-local t)
+ ;; maybe switch the context
+ (mu4e--context-autoswitch mu4e-compose-parent-message
+ mu4e-compose-context-policy)
+ (run-hooks 'mu4e-compose-pre-hook)
+ ;; this opens (or re-opens) a message with all the basic headers set.
+ (let ((winconf (current-window-configuration)))
+ (condition-case nil
+ (mu4e-draft-open compose-type original-msg switch-function)
+ (quit (set-window-configuration winconf)
+ (mu4e-message "Operation aborted")
+ (cl-return-from mu4e~compose-handler))))
+ ;; insert mail-header-separator, which is needed by message mode to separate
+ ;; headers and body. will be removed before saving to disk
+ (mu4e~draft-insert-mail-header-separator)
+
+ ;; maybe encrypt/sign replies
+ (mu4e-compose-crypto-message original-msg compose-type)
+
+ ;; include files -- e.g. when inline forwarding a message with
+ ;; attachments, we take those from the original.
+ (save-excursion
+ (goto-char (point-max)) ;; put attachments at the end
+
+ (if (and (eq compose-type 'forward) mu4e-compose-forward-as-attachment)
+ (mu4e-compose-attach-message original-msg)
+ (dolist (att includes)
+ (let ((file-name (plist-get att :file-name))
+ (mime (plist-get att :mime-type))
+ (description (plist-get att :description))
+ (disposition (plist-get att :disposition)))
+ (if file-name
+ (mml-attach-file file-name mime description disposition)
+ (mml-attach-buffer (plist-get att :buffer-name)
+ mime description disposition))))))
+
+ (mu4e~compose-set-friendly-buffer-name compose-type)
+
+ ;; bind to `mu4e-compose-parent-message' of compose buffer
+ (set (make-local-variable 'mu4e-compose-parent-message) original-msg)
+ (put 'mu4e-compose-parent-message 'permanent-local t)
+ ;; set mu4e-compose-type once more for this buffer,
+ (set (make-local-variable 'mu4e-compose-type) compose-type)
+ (put 'mu4e-compose-type 'permanent-local t)
+
+ ;; hide some headers
+ (mu4e~compose-hide-headers)
+ ;; switch on the mode
+ (mu4e-compose-mode)
+
+ ;; now jump to some useful positions, and start writing that mail!
+ (if (member compose-type '(new forward))
+ (message-goto-to)
+ ;; otherwise, it depends...
+ (pcase message-cite-reply-position
+ ((or 'above 'traditional) (message-goto-body))
+ (_ (when (message-goto-signature) (forward-line -2)))))
+
+ ;; don't allow undoing anything before this.
+ (setq buffer-undo-list nil)
+
+ (when mu4e-compose-in-new-frame
+ ;; make sure to close the frame when we're done with the message these are
+ ;; all buffer-local;
+ (push 'delete-frame message-exit-actions)
+ (push 'delete-frame message-postpone-actions))
+
+ ;; buffer is not user-modified yet
+ (set-buffer-modified-p nil))
+
+(defun mu4e~switch-back-to-mu4e-buffer ()
+ "Try to go back to some previous buffer, in the order view->headers->main."
+ (unless (eq mu4e-split-view 'single-window)
+ (if (buffer-live-p (mu4e-get-view-buffer))
+ (switch-to-buffer (mu4e-get-view-buffer))
+ (if (buffer-live-p (mu4e-get-headers-buffer))
+ (switch-to-buffer (mu4e-get-headers-buffer))
+ ;; if all else fails, back to the main view
+ (when (fboundp 'mu4e) (mu4e))))))
+
+(defun mu4e-compose-context-switch (&optional force name)
+ "Change the context for the current draft message.
+
+With NAME, switch to the context with NAME, and with FORCE non-nil,
+switch even if the switch is to the same context.
+
+Like `mu4e-context-switch' but with some changes after switching:
+1. Update the From and Organization headers as per the new context
+2. Update the message-signature as per the new context.
+
+Unlike some earlier version of this function, does _not_ update
+the draft folder for the messages, as that would require changing
+the file under our feet, which is a bit fragile."
+ (interactive "P")
+
+ (unless (derived-mode-p 'mu4e-compose-mode)
+ (mu4e-error "Only available in mu4e compose buffers"))
+
+ (let ((old-context (mu4e-context-current)))
+ (unless (and name (not force) (eq old-context name))
+ (unless (and (not force)
+ (eq old-context (mu4e-context-switch nil name)))
+ (save-excursion
+ ;; Change From / Organization if needed.
+ (message-replace-header "Organization"
+ (or (message-make-organization) "")
+ '("Subject")) ;; keep in same place
+ (message-replace-header "From"
+ (or (mu4e~draft-from-construct) ""))
+ ;; Update signature.
+ (when (message-goto-signature) ;; delete old signature.
+ (if message-signature-insert-empty-line
+ (forward-line -2) (forward-line -1))
+ (delete-region (point) (point-max)))
+ (if (and mu4e-compose-signature-auto-include mu4e-compose-signature)
+ (let ((message-signature mu4e-compose-signature))
+ (save-excursion (message-insert-signature)))))))))
+
+(defun mu4e-sent-handler (docid path)
+ "Handler called with DOCID and PATH for the just-sent message.
+For Forwarded ('Passed') and Replied messages, try to set the
+appropriate flag at the message forwarded or replied-to."
+ (mu4e~compose-set-parent-flag path)
+ (when (file-exists-p path) ;; maybe the draft was not saved at all
+ (mu4e--server-remove docid))
+ ;; kill any remaining buffers for the draft file, or they will hang around...
+ ;; this seems a bit hamfisted...
+ (when message-kill-buffer-on-exit
+ (dolist (buf (buffer-list))
+ (and (buffer-file-name buf)
+ (string= (buffer-file-name buf) path)
+ (kill-buffer buf))))
+ (mu4e~switch-back-to-mu4e-buffer)
+ (mu4e-message "Message sent"))
+
+(defun mu4e-message-kill-buffer ()
+ "Wrapper around `message-kill-buffer'.
+It restores mu4e window layout after killing the compose-buffer."
+ (interactive)
+ (let ((current-buffer (current-buffer)))
+ (message-kill-buffer)
+ ;; Compose buffer killed
+ (when (not (equal current-buffer (current-buffer)))
+ ;; Restore mu4e
+ (if mu4e-compose-in-new-frame
+ (delete-frame)
+ (mu4e~switch-back-to-mu4e-buffer)))))
+
+(defun mu4e~compose-set-parent-flag (path)
+ "Set flags for replied-to and forwarded for the message at PATH.
+That is, set the `replied' \"R\" flag on messages we replied to,
+and the `passed' \"F\" flag on message we have forwarded.
+
+If a message has an \"In-Reply-To\" header, it is considered a
+reply to the message with the corresponding message id.
+Otherwise, if it does not have an \"In-Reply-To\" header, but
+does have a \"References:\" header, it is considered to be a
+forward message for the message corresponding with the /last/
+message-id in the references header.
+
+If the message has been determined to be either a forwarded
+message or a reply, we instruct the server to update that message
+with resp. the \"P\" (passed) flag for a forwarded message, or
+the \"R\" flag for a replied message. The original messages are
+also marked as Seen.
+
+Function assumes that it is executed in the context of the
+message buffer."
+ (let ((buf (find-file-noselect path)))
+ (when buf
+ (with-current-buffer buf
+ (message-narrow-to-headers-or-head)
+ (let ((in-reply-to (message-fetch-field "in-reply-to"))
+ (forwarded-from)
+ (references (message-fetch-field "references")))
+ (unless in-reply-to
+ (when references
+ (with-temp-buffer ;; inspired by `message-shorten-references'.
+ (insert references)
+ (goto-char (point-min))
+ (let ((refs))
+ (while (re-search-forward "<[^ <]+@[^ <]+>" nil t)
+ (push (match-string 0) refs))
+ ;; the last will be the first
+ (setq forwarded-from (car refs))))))
+ ;; remove the <>
+ (when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to))
+ (mu4e--server-move (match-string 1 in-reply-to) nil "+R-N"))
+ (when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from))
+ (mu4e--server-move (match-string 1 forwarded-from) nil "+P-N")))))))
+
+(defun mu4e-compose (compose-type)
+ "Start composing a message of COMPOSE-TYPE.
+COMPOSE-TYPE is a symbol, one of `reply', `forward', `edit',
+`resend' `new'. All but `new' take the message at point as input.
+Symbol `edit' is only allowed for draft messages."
+ (let ((msg (mu4e-message-at-point 'noerror)))
+ ;; some sanity checks
+ (unless (or msg (eq compose-type 'new))
+ (mu4e-warn "No message at point"))
+ (unless (member compose-type '(reply forward edit resend new))
+ (mu4e-error "Invalid compose type '%S'" compose-type))
+ (when (and (eq compose-type 'edit)
+ (not (member 'draft (mu4e-message-field msg :flags))))
+ (mu4e-warn "Editing is only allowed for draft messages"))
+
+ ;; 'new is special, since it takes no existing message as arg; therefore, we
+ ;; don't need to involve the backend, and call the handler *directly*
+ (if (eq compose-type 'new)
+ (mu4e~compose-handler 'new)
+ ;; otherwise, we need the doc-id
+ (let* ((docid (mu4e-message-field msg :docid))
+ ;; decrypt (or not), based on `mu4e-decryption-policy'.
+ (decrypt
+ (and (member 'encrypted (mu4e-message-field msg :flags))
+ (if (eq mu4e-decryption-policy 'ask)
+ (yes-or-no-p (mu4e-format "Decrypt message?"))
+ mu4e-decryption-policy))))
+ ;; if there's a visible view window, select that before starting
+ ;; composing a new message, so that one will be replaced by the compose
+ ;; window. The 10-or-so line headers buffer is not a good place to write
+ ;; it...
+ (unless (eq mu4e-split-view 'single-window)
+ (let ((viewwin (get-buffer-window (mu4e-get-view-buffer))))
+ (when (window-live-p viewwin)
+ (select-window viewwin))))
+ ;; talk to the backend
+ (mu4e--server-compose compose-type decrypt docid)))))
+
+(defun mu4e-compose-reply ()
+ "Compose a reply for the message at point in the headers buffer."
+ (interactive)
+ (mu4e-compose 'reply))
+
+(defun mu4e-compose-forward ()
+ "Forward the message at point in the headers buffer."
+ (interactive)
+ (mu4e-compose 'forward))
+
+(defun mu4e-compose-edit ()
+ "Edit the draft message at point in the headers buffer.
+This is only possible if the message at point is, in fact, a
+draft message."
+ (interactive)
+ (mu4e-compose 'edit))
+
+(defun mu4e-compose-resend ()
+ "Resend the message at point in the headers buffer."
+ (interactive)
+ (mu4e-compose 'resend))
+
+(defun mu4e-compose-new ()
+ "Start writing a new message."
+ (interactive)
+ (mu4e-compose 'new))
+
+\f
+;;; Compose Mail
+;; mu4e-compose-func and mu4e-send-func are wrappers so we can set ourselves
+;; as default emacs mailer (define-mail-user-agent etc.)
+
+(declare-function mu4e "mu4e")
+
+;;;###autoload
+(defun mu4e~compose-mail (&optional to subject other-headers _continue
+ switch-function yank-action
+ _send-actions _return-action)
+ "This is mu4e's implementation of `compose-mail'.
+Quoting its docstring:
+
+Start composing a mail message to send. This uses the user's
+chosen mail composition package as selected with the variable
+`mail-user-agent'. The optional arguments TO and SUBJECT specify
+recipients and the initial Subject field, respectively.
+
+OTHER-HEADERS is an alist specifying additional
+header fields. Elements look like (HEADER . VALUE) where both
+HEADER and VALUE are strings.
+
+CONTINUE, if non-nil, says to continue editing a message already
+being composed. Interactively, CONTINUE is the prefix argument.
+
+SWITCH-FUNCTION, if non-nil, is a function to use to
+switch to and display the buffer used for mail composition.
+
+YANK-ACTION, if non-nil, is an action to perform, if and when
+necessary, to insert the raw text of the message being replied
+to. It has the form (FUNCTION . ARGS). The user agent will apply
+FUNCTION to ARGS, to insert the raw text of the original message.
+\(The user agent will also run `mail-citation-hook', *after* the
+original text has been inserted in this way.)
+
+SEND-ACTIONS is a list of actions to call when the message is sent.
+Each action has the form (FUNCTION . ARGS).
+
+RETURN-ACTION, if non-nil, is an action for returning to the
+caller. It has the form (FUNCTION . ARGS). The function is
+called after the mail has been sent or put aside, and the mail
+buffer buried."
+
+ (unless (mu4e-running-p)
+ (mu4e))
+
+ ;; create a new draft message 'resetting' (as below) is not actually needed in
+ ;; this case, but let's prepare for the re-edit case as well
+ (mu4e~compose-handler 'new nil nil switch-function)
+
+ (when (message-goto-to) ;; reset to-address, if needed
+ (message-delete-line))
+ (message-add-header (concat "To: " to "\n"))
+
+ (when (message-goto-subject) ;; reset subject, if needed
+ (message-delete-line))
+ (message-add-header (concat "Subject: " subject "\n"))
+
+ ;; add any other headers specified
+ (seq-each (lambda(hdr)
+ (let ((field (capitalize(car hdr))) (value (cdr hdr)))
+ ;; fix in-reply without <>
+ (when (and (string= field "In-Reply-To")
+ (string-match-p "\\`[^ @]+@[^ @]+\\'" value)
+ (not (string-match-p "\\`<.*>\\'" value)))
+ (setq value (concat "<" value ">")))
+ (message-add-header (concat (capitalize field) ": " value "\n"))))
+ other-headers)
+
+ ;; yank message
+ (if (bufferp yank-action)
+ (list 'insert-buffer yank-action)
+ yank-action)
+
+ ;; try to put the user at some reasonable spot...
+ (if (not to)
+ (message-goto-to)
+ (if (not subject)
+ (message-goto-subject)
+ (message-goto-body))))
+
+;; happily, we can re-use most things from message mode
+;;;###autoload
+(define-mail-user-agent 'mu4e-user-agent
+ 'mu4e~compose-mail
+ 'message-send-and-exit
+ 'message-kill-buffer
+ 'message-send-hook)
+;; Without this `mail-user-agent' cannot be set to `mu4e-user-agent'
+;; through customize, as the custom type expects a function. Not
+;; sure whether this function is actually ever used; if it is then
+;; returning the symbol is probably the correct thing to do, as other
+;; such functions suggest.
+(defun mu4e-user-agent ()
+ "Return the `mu4e-user-agent' symbol."
+ 'mu4e-user-agent)
+
+;;; Go to bottom / top
+
+(defun mu4e-compose-goto-top (&optional arg)
+ "Go to the beginning of the message or buffer.
+Go to the beginning of the message or, if already there, go to the
+beginning of the buffer.
+
+Push mark at previous position, unless either a \\[universal-argument] prefix
+is supplied, or Transient Mark mode is enabled and the mark is active."
+ (interactive "P")
+ (or arg
+ (region-active-p)
+ (push-mark))
+ (let ((old-position (point)))
+ (message-goto-body)
+ (when (equal (point) old-position)
+ (goto-char (point-min)))))
+
+(define-key mu4e-compose-mode-map
+ (vector 'remap 'beginning-of-buffer) 'mu4e-compose-goto-top)
+
+(defun mu4e-compose-goto-bottom (&optional arg)
+ "Go to the end of the message or buffer.
+Go to the end of the message (before signature) or, if already there, go to the
+end of the buffer.
+
+Push mark at previous position, unless either a \\[universal-argument] prefix
+is supplied, or Transient Mark mode is enabled and the mark is active."
+ (interactive "P")
+ (or arg
+ (region-active-p)
+ (push-mark))
+ (let ((old-position (point))
+ (message-position (save-excursion (message-goto-body) (point))))
+ (goto-char (point-max))
+ (when (re-search-backward message-signature-separator message-position t)
+ (forward-line -1))
+ (when (equal (point) old-position)
+ (goto-char (point-max)))))
+
+(define-key mu4e-compose-mode-map
+ (vector 'remap 'end-of-buffer) 'mu4e-compose-goto-bottom)
+
+;;; _
+(provide 'mu4e-compose)
+;;; mu4e-compose.el ends here
--- /dev/null
+;; auto-generated
+
+(defconst mu4e-mu-version "@VERSION@"
+ "Required mu binary version; mu4e's version must agree with this.")
+
+(defconst mu4e-builddir "@abs_top_builddir@"
+ "Top-level build directory.")
+
+(defconst mu4e-doc-dir "@MU_DOC_DIR@"
+ "Mu4e's data-dir.")
+
+(provide 'mu4e-config)
--- /dev/null
+;;; mu4e-contacts.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Utility functions used in the mu4e
+
+;;; Code:
+(require 'cl-lib)
+(require 'mu4e-helpers)
+(require 'mu4e-update)
+\f
+
+;;; Configuration
+(defcustom mu4e-compose-complete-addresses t
+ "Whether to do auto-completion of e-mail addresses."
+ :type 'boolean
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-only-personal nil
+ "Whether to consider only \"personal\" e-mail addresses for completion.
+That is, addresses from messages where user was explicitly in one
+of the address fields (this excludes mailing list messages).
+These addresses are the ones specified with \"mu init\"."
+ :type 'boolean
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-only-after "2018-01-01"
+ "Consider only contacts last seen after this date.
+
+Date must be a string of the form YYYY-MM-DD.
+
+This is useful for limiting a potentially enormous set of
+contacts for auto-completion to just those that are present in
+the e-mail corpus in recent times. Set to nil to not have any
+time-based restriction."
+ :type 'string
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-max nil
+ "Consider only the top-n contacts.
+After considering the other
+constraints (`mu4e-compose-complete-addresses' and
+`mu4e-compose-complete-only-after'), pick only the highest-ranked
+<n>.
+
+This reduces start-up time and memory usage. Set to nil for no
+limits."
+ :type 'string
+ :group 'mu4e-compose)
+
+;; names and mail-addresses can be mapped onto their canonical
+;; counterpart. use the customizeable function
+;; mu4e-canonical-contact-function to do that. below the identity
+;; function for mapping a contact onto the canonical one.
+(defun mu4e-contact-identity (contact)
+ "Return the name and the mail-address of a CONTACT.
+It is used as the identity function for converting contacts to
+their canonical counterpart; useful as an example."
+ (let ((name (plist-get contact :name))
+ (mail (plist-get contact :mail)))
+ (list :name name :mail mail)))
+
+(make-obsolete-variable 'mu4e-contact-rewrite-function
+ "mu4e-contact-process-function (see docstring)"
+ "mu4e 1.3.2")
+(make-obsolete-variable 'mu4e-compose-complete-ignore-address-regexp
+ "mu4e-contact-process-function (see docstring)"
+ "mu4e 1.3.2")
+
+(defcustom mu4e-contact-process-function
+ (lambda(addr)
+ (cond
+ ((string-match-p "reply" addr)
+ ;; no-reply adresses are not useful of course, but neither are are
+ ;; reply-xxxx addresses since they're autogenerated only useful for direct
+ ;; replies.
+ nil)
+ (t addr)))
+ "Function for processing contact information for use in auto-completion.
+
+The function receives the contact as a string, e.g \"Foo Bar
+ <foo.bar@example.com>\" \"cuux@example.com\"
+
+The function should return either:
+- nil: do not use this contact for completion
+- the (possibly rewritten) address, which must be
+an RFC-2822-compatible e-mail address."
+ :type 'function
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-reply-ignore-address
+ '("no-?reply")
+ "Addresses to prune when doing wide replies.
+
+This can be a regexp matching the address, a list of regexps or a
+predicate function. A value of nil keeps all the addresses."
+ :type '(choice
+ (const nil)
+ function
+ string
+ (repeat string))
+ :group 'mu4e-compose)
+
+\f
+;;; Internal variables
+(defvar mu4e--contacts-tstamp "0"
+ "Timestamp for the most recent contacts update." )
+
+(defvar mu4e--contacts-set nil
+ "Set with the full contact addresses for autocompletion.")
+\f
+;;; user mail address
+(defun mu4e-personal-addresses(&optional no-regexp)
+ "Get the list user's personal addresses, as passed to mu init.
+The address are either plain e-mail address or /regular
+ expressions/. When NO-REGEXP is non-nil, do not include regexp
+ address patterns (if any)."
+ (seq-remove
+ (lambda(addr) (and no-regexp (string-match-p "^/.*/" addr)))
+ (when (mu4e-server-properties)
+ (plist-get (mu4e-server-properties) :personal-addresses))))
+
+(defun mu4e-personal-address-p (addr)
+ "Is ADDR a personal address?
+Evaluate to nil if ADDR matches any of the personal addresses.
+Uses (mu4e-personal-addresses) for the addresses with both the plain
+addresses and /regular expressions/."
+ (when addr
+ (seq-find
+ (lambda (m)
+ (if (string-match "/\\(.*\\)/" m)
+ (let ((rx (match-string 1 m))
+ (case-fold-search t))
+ (if (string-match rx addr) t nil))
+ (eq t (compare-strings addr nil nil m nil nil 'case-insensitive))))
+ (mu4e-personal-addresses))))
+
+(define-obsolete-function-alias 'mu4e-user-mail-address-p
+ 'mu4e-personal-address-p "1.5.5")
+
+
+;; don't use the older vars anymore
+(make-obsolete-variable 'mu4e-user-mail-address-regexp
+ 'mu4e-user-mail-address-list "0.9.9.x")
+(make-obsolete-variable 'mu4e-my-email-addresses
+ 'mu4e-user-mail-address-list "0.9.9.x")
+(make-obsolete-variable 'mu4e-user-mail-address-list
+ "determined by server; see `mu4e-personal-addresses'."
+ "1.3.8")
+
+\f
+;; Helpers
+
+
+;;; RFC2822 handling of phrases in mail-addresses
+;;
+;; The optional display-name contains a phrase, it sits before the
+;; angle-addr as specified in RFC2822 for email-addresses in header
+;; fields. Contributed by jhelberg.
+
+(defun mu4e--rfc822-phrase-type (ph)
+ "Return an atom or quoted-string for the phrase PH.
+This checks for empty string first. Then quotes around the phrase
+\(returning symbol `rfc822-quoted-string'). Then whether there is
+a quote inside the phrase (returning symbol
+`rfc822-containing-quote').
+
+The reverse of the RFC atext definition is then tested. If it
+matches, nil is returned, if not, it returns a symbol
+`rfc822-atom'."
+ (cond
+ ((= (length ph) 0) 'rfc822-empty)
+ ((= (aref ph 0) ?\")
+ (if (string-match "\"\\([^\"\\\n]\\|\\\\.\\|\\\\\n\\)*\"" ph)
+ 'rfc822-quoted-string
+ 'rfc822-containing-quote)) ; starts with quote, but doesn't end with one
+ ((string-match-p "[\"]" ph) 'rfc822-containing-quote)
+ ((string-match-p "[\000-\037()\*<>@,;:\\\.]+" ph) nil)
+ (t 'rfc822-atom)))
+
+(defun mu4e--rfc822-quote-phrase (ph)
+ "Quote an RFC822 phrase PH only if necessary.
+Atoms and quoted strings don't need quotes. The rest do. In
+case a phrase contains a quote, it will be escaped."
+ (let ((type (mu4e--rfc822-phrase-type ph)))
+ (cond
+ ((eq type 'rfc822-atom) ph)
+ ((eq type 'rfc822-quoted-string) ph)
+ ((eq type 'rfc822-containing-quote)
+ (format "\"%s\""
+ (replace-regexp-in-string "\"" "\\\\\"" ph)))
+ (t (format "\"%s\"" ph)))))
+
+(defsubst mu4e-contact-name (contact)
+ "Get the name of this CONTACT, or nil."
+ (plist-get contact :name))
+
+(defsubst mu4e-contact-email (contact)
+ "Get the name of this CONTACT, or nil."
+ (plist-get contact :email))
+
+(defsubst mu4e-contact-cons (contact)
+ "Convert a CONTACT plist into a old-style (name . email)."
+ (cons
+ (mu4e-contact-name contact)
+ (mu4e-contact-email contact)))
+
+(defsubst mu4e-contact-make (name email)
+ "Creata contact plist from NAME and EMAIL."
+ `(:name ,name :email ,email))
+
+(defun mu4e-contact-full (contact)
+ "Get the full combination of name and email address from CONTACT."
+ (let* ((email (mu4e-contact-email contact))
+ (name (mu4e-contact-name contact)))
+ (if (and name (> (length name) 0))
+ (format "%s <%s>" (mu4e--rfc822-quote-phrase name) email)
+ email)))
+
+\f
+(defun mu4e--update-contacts (contacts &optional tstamp)
+ "Receive a sorted list of CONTACTS newer than TSTAMP.
+Update an internal set with it.
+
+This is used by the completion function in mu4e-compose."
+ (let ((n 0))
+ (unless mu4e--contacts-set
+ (setq mu4e--contacts-set (make-hash-table :test 'equal :weakness nil
+ :size (length contacts))))
+ (dolist (contact contacts)
+ (cl-incf n)
+ (when (functionp mu4e-contact-process-function)
+ (setq contact (funcall mu4e-contact-process-function contact)))
+ (when contact ;; note the explicit deccode; the strings we get are
+ ;; utf-8, but emacs doesn't know yet.
+ (puthash (decode-coding-string contact 'utf-8) t mu4e--contacts-set)))
+ (setq mu4e--contacts-tstamp (or tstamp "0"))
+ (unless (zerop n)
+ (mu4e-index-message "Contacts updated: %d; total %d"
+ n (hash-table-count mu4e--contacts-set)))))
+
+(defun mu4e-contacts-info ()
+ "Display information about the contacts-cache.
+For testing/debugging."
+ (interactive)
+ (with-current-buffer (get-buffer-create "*mu4e-contacts-info*")
+ (erase-buffer)
+ (insert (format "complete addresses: %s\n"
+ (if mu4e-compose-complete-addresses "yes" "no")))
+ (insert (format "only personal addresses: %s\n"
+ (if mu4e-compose-complete-only-personal "yes" "no")))
+ (insert (format "only addresses seen after: %s\n"
+ (or mu4e-compose-complete-only-after "no restrictions")))
+
+ (when mu4e--contacts-set
+ (insert (format "number of contacts cached: %d\n\n"
+ (hash-table-count mu4e--contacts-set)))
+ (maphash (lambda (contact _)
+ (insert (format "%s\n" contact))) mu4e--contacts-set))
+ (pop-to-buffer "*mu4e-contacts-info*")))
+
+(declare-function mu4e--server-contacts "mu4e-server")
+
+(defun mu4e--request-contacts-maybe ()
+ "Maybe update the set of contacts for autocompletion.\0
+
+If `mu4e-compose-complete-addresses' is non-nil, get/update the
+list of contacts we use for autocompletion; otherwise, do
+nothing."
+ (when mu4e-compose-complete-addresses
+ (mu4e--server-contacts
+ mu4e-compose-complete-only-personal
+ mu4e-compose-complete-only-after
+ mu4e-compose-complete-max
+ mu4e--contacts-tstamp)))
+
+(provide 'mu4e-contacts)
+;;; mu4e-contacts.el ends here
--- /dev/null
+;;; mu4e-context.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; A mu4e 'context' is a set of variable-settings and functions, which can be
+;; used e.g. to switch between accounts.
+
+;;; Code:
+
+(require 'mu4e-helpers)
+
+\f
+;;; Configuration
+(defcustom mu4e-context-policy 'ask-if-none
+ "The policy to determine the context when entering the mu4e main view.
+
+If the value is `always-ask', ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is used. Otherwise, if none of the
+contexts match, we have the following choices:
+
+- `pick-first': pick the first of the contexts available (ie. the default)
+- `ask': ask the user
+- `ask-if-none': ask if there is no context yet, otherwise leave it as it is
+- nil: return nil; leaves the current context as is.
+
+Also see `mu4e-compose-context-policy'."
+ :type '(choice
+ (const :tag "Always ask what context to use, even if one matches"
+ always-ask)
+ (const :tag "Ask if none of the contexts match" ask)
+ (const :tag "Ask when there's no context yet" ask-if-none)
+ (const :tag "Pick the first context if none match" pick-first)
+ (const :tag "Don't change the context when none match" nil))
+ :group 'mu4e)
+
+
+(defvar mu4e-contexts nil
+ "The list of `mu4e-context' objects describing mu4e's contexts.")
+
+(defvar mu4e-context-changed-hook nil
+ "Hook run just *after* the context changed.")
+
+(defface mu4e-context-face
+ '((t :inherit mu4e-title-face :weight bold))
+ "Face for displaying the context in the modeline."
+ :group 'mu4e-faces)
+
+(defvar mu4e--context-current nil
+ "The current context.
+Internal; use `mu4e-context-switch' to change it.")
+\f
+(defun mu4e-context-current (&optional output)
+ "Get the currently active context, or nil if there is none.
+When OUTPUT is non-nil, echo the name of the current context or
+none."
+ (interactive "p")
+ (let ((ctx mu4e--context-current))
+ (when output
+ (mu4e-message "Current context: %s"
+ (if ctx (mu4e-context-name ctx) "<none>")))
+ ctx))
+
+(defun mu4e-context-label ()
+ "Propertized string with the current context name.
+An empty string \"\" if there is none."
+ (if (mu4e-context-current)
+ (concat "[" (propertize (mu4e-quote-for-modeline
+ (mu4e-context-name (mu4e-context-current)))
+ 'face 'mu4e-context-face) "]") ""))
+
+(cl-defstruct mu4e-context
+ "A mu4e context object with the following members:
+- `name': the name of the context, eg. \"Work\" or \"Private\".
+- `enter-func': a parameterless function invoked when entering
+ this context, or nil
+- `leave-func':a parameterless function invoked when leaving this
+ context, or nil
+- `match-func': a function called when composing a new message,
+ that takes a message plist for the message replied to or
+ forwarded, and nil otherwise. Before composing a new message,
+ `mu4e' switches to the first context for which `match-func'
+ returns t.
+- `vars': variables to set when entering context."
+ name ;; name of the context, e.g. "work"
+ (enter-func nil) ;; function invoked when entering the context
+ (leave-func nil) ;; function invoked when leaving the context
+ (match-func nil) ;; function that takes a msg-proplist, and return t
+ ;; if it matches, nil otherwise
+ vars) ;; alist of variables.
+
+
+(defun mu4e--context-ask-user (prompt)
+ "Let user choose some context based on its name with PROMPT."
+ (when mu4e-contexts
+ (let* ((names (seq-map (lambda (context)
+ (cons (mu4e-context-name context) context))
+ mu4e-contexts))
+ (context (mu4e-read-option prompt names)))
+ (or context (mu4e-error "No such context")))))
+
+(defun mu4e-context-switch (&optional force name)
+ "Switch to a context with NAME.
+Context must be part of `mu4e-contexts'; if NAME is nil, query user.
+
+If the new context is the same and the current context, only
+switch (run associated functions) when prefix argument FORCE is
+non-nil."
+ (interactive "P")
+ (unless mu4e-contexts
+ (mu4e-error "No contexts defined"))
+ (let* ((names (seq-map (lambda (context)
+ (cons (mu4e-context-name context) context))
+ mu4e-contexts))
+ (context
+ (if name
+ (cdr-safe (assoc name names))
+ (mu4e--context-ask-user "Switch to context: "))))
+ (unless context (mu4e-error "No such context"))
+ ;; if new context is same as old one one switch with FORCE is set.
+ (when (or force (not (eq context (mu4e-context-current))))
+ (when (and (mu4e-context-current)
+ (mu4e-context-leave-func mu4e--context-current))
+ (funcall (mu4e-context-leave-func mu4e--context-current)))
+ ;; enter the new context
+ (when (mu4e-context-enter-func context)
+ (funcall (mu4e-context-enter-func context)))
+ (when (mu4e-context-vars context)
+ (mapc (lambda (cell)
+ (set (car cell) (cdr cell)))
+ (mu4e-context-vars context)))
+ (setq mu4e--context-current context)
+
+ (run-hooks 'mu4e-context-changed-hook)
+ (mu4e-message "Switched context to %s" (mu4e-context-name context))
+ (force-mode-line-update))
+ context))
+
+(defun mu4e--context-autoswitch (&optional msg policy)
+ "Automatically switch to some context.
+
+When contexts are defined but there is no context yet, switch to
+the first whose :match-func return non-nil. If none of them
+match, return the first. For MSG and POLICY, see
+`mu4e-context-determine'."
+ (when mu4e-contexts
+ (let ((context (mu4e-context-determine msg policy)))
+ (when context (mu4e-context-switch
+ nil (mu4e-context-name context))))))
+
+(defun mu4e-context-determine (msg &optional policy)
+ "Return the first context where match-func evaluate to non-nil.
+
+MSG points to the plist for the message replied to or forwarded,
+or nil if there is no such MSG; similar to what
+`mu4e-compose-pre-hook' does.
+
+POLICY specifies how to do the determination. If POLICY is
+`always-ask', we ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is returned. If none of the contexts
+match, POLICY determines what to do:
+
+- `pick-first': pick the first of the contexts available
+- `ask': ask the user
+- `ask-if-none': ask if there is no context yet
+- otherwise, return nil. Effectively, this leaves the current context
+as it is."
+ (when mu4e-contexts
+ (if (eq policy 'always-ask)
+ (mu4e--context-ask-user "Select context: ")
+ (or ;; is there a matching one?
+ (seq-find (lambda (context)
+ (when (mu4e-context-match-func context)
+ (funcall (mu4e-context-match-func context) msg)))
+ mu4e-contexts)
+ ;; no context found yet; consult policy
+ (pcase policy
+ ('pick-first (car mu4e-contexts))
+ ('ask (mu4e--context-ask-user "Select context: "))
+ ('ask-if-none (or (mu4e-context-current)
+ (mu4e--context-ask-user "Select context: ")))
+ (_ nil))))))
+
+(defun mu4e-context-in-modeline ()
+ "Display the mu4e-context (if any) in a (buffer-specific)
+global-mode-line."
+ (add-to-list
+ (make-local-variable 'global-mode-string)
+ '(:eval (mu4e-context-label))))
+
+(defmacro with-mu4e-context-vars (context &rest body)
+ "Evaluate BODY, with variables let-bound for CONTEXT (if any).
+`funcall'."
+ (declare (indent 2))
+ `(let* ((vars (and ,context (mu4e-context-vars ,context))))
+ (cl-progv ;; XXX: perhaps use eval's lexical environment instead of progv?
+ (mapcar (lambda(cell) (car cell)) vars)
+ (mapcar (lambda(cell) (cdr cell)) vars)
+ (eval ,@body))))
+
+(define-minor-mode mu4e-context-minor-mode
+ "Mode for switching the mu4e context."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ :keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd";") #'mu4e-context-switch)
+ map)
+ (mu4e-context-in-modeline))
+
+;;;
+(provide 'mu4e-context)
+;;; mu4e-context.el ends here
--- /dev/null
+;;; mu4e-contrib.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013-2022 Dirk-Jan C. Binnema
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Some user-contributed functions for mu4e
+
+;;; Code:
+
+(require 'mu4e-headers)
+(require 'mu4e-view)
+(require 'bookmark)
+(require 'eshell)
+
+\f
+;;; Various simple commands
+(defun mu4e-headers-mark-all-unread-read ()
+ "Put a ! \(read) mark on all visible unread messages."
+ (interactive)
+ (mu4e-headers-mark-for-each-if
+ (cons 'read nil)
+ (lambda (msg _param)
+ (memq 'unread (mu4e-msg-field msg :flags)))))
+
+(defun mu4e-headers-flag-all-read ()
+ "Flag all visible messages as \"read\"."
+ (interactive)
+ (mu4e-headers-mark-all-unread-read)
+ (mu4e-mark-execute-all t))
+
+(defun mu4e-headers-mark-all ()
+ "Mark all headers for some action.
+Ask user what action to execute."
+ (interactive)
+ (mu4e-headers-mark-for-each-if
+ (cons 'something nil)
+ (lambda (_msg _param) t))
+ (mu4e-mark-execute-all))
+
+
+\f
+;;; Bogofilter/SpamAssassin
+;;
+;; Support for handling spam with Bogofilter with the possibility
+;; to define it for SpamAssassin, contributed by Gour.
+;;
+;; To add the actions to the menu, you can use something like:
+;;
+;; (add-to-list 'mu4e-headers-actions
+;; '("sMark as spam" . mu4e-register-msg-as-spam) t)
+;; (add-to-list 'mu4e-headers-actions
+;; '("hMark as ham" . mu4e-register-msg-as-ham) t)
+
+(defvar mu4e-register-as-spam-cmd nil
+ "Command for invoking spam processor to register message as spam.
+For example for bogofilter, use \"/usr/bin/bogofilter -Ns < %s\"")
+
+(defvar mu4e-register-as-ham-cmd nil
+ "Command for invoking spam processor to register message as ham.
+For example for bogofile, use \"/usr/bin/bogofilter -Sn < %s\"")
+
+(defun mu4e-register-msg-as-spam (msg)
+ "Register MSG as spam."
+ (interactive)
+ (let* ((path (shell-quote-argument (mu4e-message-field msg :path)))
+ (command (format mu4e-register-as-spam-cmd path)))
+ (shell-command command))
+ (mu4e-mark-at-point 'delete nil))
+
+(defun mu4e-register-msg-as-ham (msg)
+ "Register MSG as ham."
+ (interactive)
+ (let* ((path (shell-quote-argument(mu4e-message-field msg :path)))
+ (command (format mu4e-register-as-ham-cmd path)))
+ (shell-command command))
+ (mu4e-mark-at-point 'something nil))
+
+;; (add-to-list 'mu4e-view-actions
+;; '("sMark as spam" . mu4e-view-register-msg-as-spam) t)
+;; (add-to-list 'mu4e-view-actions
+;; '("hMark as ham" . mu4e-view-register-msg-as-ham) t)
+
+(defun mu4e-view-register-msg-as-spam (msg)
+ "Register MSG as spam (view mode)."
+ (interactive)
+ (let* ((path (shell-quote-argument (mu4e-message-field msg :path)))
+ (command (format mu4e-register-as-spam-cmd path)))
+ (shell-command command))
+ (mu4e-view-mark-for-delete))
+
+(defun mu4e-view-register-msg-as-ham (msg)
+ "Mark MSG as ham (view mode)."
+ (interactive)
+ (let* ((path (shell-quote-argument(mu4e-message-field msg :path)))
+ (command (format mu4e-register-as-ham-cmd path)))
+ (shell-command command))
+ (mu4e-view-mark-for-something))
+
+\f
+;;; Eshell functions
+;;
+;; Code for `gnus-dired-attached' modified to run from eshell,
+;; allowing files to be attached to an email via mu4e using the
+;; eshell. Does not depend on gnus.
+
+
+(defun mu4e--active-composition-buffers ()
+ "Return all active mu4e composition buffers."
+ (let (buffers)
+ (save-excursion
+ (dolist (buffer (buffer-list t))
+ (set-buffer buffer)
+ (when (eq major-mode 'mu4e-compose-mode)
+ (push (buffer-name buffer) buffers))))
+ (nreverse buffers)))
+
+(defun eshell/mu4e-attach (&rest args)
+ "Attach files to a mu4e message using eshell with ARGS.
+If no mu4e buffers found, compose a new message and then attach
+the file."
+ (let ((destination nil)
+ (files-str nil)
+ (bufs nil)
+ ;; Remove directories from the list
+ (files-to-attach
+ (delq nil (mapcar
+ (lambda (f) (if (or (not (file-exists-p f))
+ (file-directory-p f))
+ nil
+ (expand-file-name f)))
+ (eshell-flatten-list (reverse args))))))
+ ;; warn if user tries to attach without any files marked
+ (if (null files-to-attach)
+ (error "No files to attach")
+ (setq files-str
+ (mapconcat
+ (lambda (f) (file-name-nondirectory f))
+ files-to-attach ", "))
+ (setq bufs (mu4e--active-composition-buffers))
+ ;; set up destination mail composition buffer
+ (if (and bufs
+ (y-or-n-p "Attach files to existing mail composition buffer? "))
+ (setq destination
+ (if (= (length bufs) 1)
+ (get-buffer (car bufs))
+ (let ((prompt (mu4e-format "%s" "Attach to buffer")))
+ (substring-no-properties
+ (funcall mu4e-completing-read-function prompt
+ bufs)))))
+ ;; setup a new mail composition buffer
+ (if (y-or-n-p "Compose new mail and attach this file? ")
+ (progn (mu4e-compose-new)
+ (setq destination (current-buffer)))))
+ ;; if buffer was found, set buffer to destination buffer, and attach files
+ (if (not (eq destination 'nil))
+ (progn (set-buffer destination)
+ (goto-char (point-max)) ; attach at end of buffer
+ (while files-to-attach
+ (mml-attach-file (car files-to-attach)
+ (or (mm-default-file-encoding
+ (car files-to-attach))
+ "application/octet-stream") nil)
+ (setq files-to-attach (cdr files-to-attach)))
+ (message "Attached file(s) %s" files-str))
+ (message "No buffer to attach file to.")))))
+
+;;; _
+(provide 'mu4e-contrib)
+;;; mu4e-contrib.el ends here
--- /dev/null
+;;; mu4e-draft.el -- part of mu4e, the mu mail user agent for emacs -*- lexical-binding: t -*-
+;;
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file, various functions to create draft messages
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'mu4e-message)
+(require 'mu4e-contacts)
+(require 'mu4e-folders)
+(require 'message) ;; mail-header-separator
+
+\f
+;;; Configuration
+(defgroup mu4e-compose nil
+ "Customizations for composing/sending messages."
+ :group 'mu4e)
+
+(defcustom mu4e-compose-reply-recipients 'ask
+ "Which recipients to use when replying to a message.
+May be a symbol `ask', `all', `sender'. Note that that only
+applies to non-mailing-list message; for those, mu4e always
+asks."
+ :type '(choice ask
+ all
+ sender)
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-reply-to-address nil
+ "The Reply-To address.
+Useful when this is not equal to the From: address."
+ :type 'string
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-forward-as-attachment nil
+ "Whether to forward messages as attachments instead of inline."
+ :type 'boolean
+ :group 'mu4e-compose)
+
+;; backward compatibility
+(make-obsolete-variable 'mu4e-reply-to-address
+ 'mu4e-compose-reply-to-address
+ "v0.9.9")
+
+(defcustom mu4e-compose-keep-self-cc nil
+ "When non-nil. keep your e-mail address in Cc: when replying."
+ :type 'boolean
+ :group 'mu4e-compose)
+
+(defvar mu4e-compose-parent-message nil
+ "The parent message plist.
+This is the message being replied to, forwarded or edited; used
+in `mu4e-compose-pre-hook'. For new messages, it is nil.")
+
+(make-obsolete-variable 'mu4e-auto-retrieve-keys "no longer used." "1.3.1")
+
+(defcustom mu4e-decryption-policy t
+ "Policy for dealing with replying/forwarding encrypted parts.
+The setting is a symbol:
+ * t: try to decrypt automatically
+ * `ask': ask before decrypting anything
+ * nil: don't try to decrypt anything."
+ :type '(choice (const :tag "Try to decrypt automatically" t)
+ (const :tag "Ask before decrypting anything" ask)
+ (const :tag "Don't try to decrypt anything" nil))
+ :group 'mu4e-compose)
+\f
+;;; Composing / Sending messages
+
+(defcustom mu4e-sent-messages-behavior 'sent
+ "Determines what mu4e does with sent messages.
+
+This is one of the symbols:
+* `sent' move the sent message to the Sent-folder (`mu4e-sent-folder')
+* `trash' move the sent message to the Trash-folder (`mu4e-trash-folder')
+* `delete' delete the sent message.
+
+Note, when using GMail/IMAP, you should set this to either
+`trash' or `delete', since GMail already takes care of keeping
+copies in the sent folder.
+
+Alternatively, `mu4e-sent-messages-behavior' can be a function
+which takes no arguments, and which should return one of the mentioned
+symbols, for example:
+
+ (setq mu4e-sent-messages-behavior (lambda ()
+ (if (string= (message-sendmail-envelope-from) \"foo@example.com\")
+ \='delete \='sent)))
+
+The various `message-' functions from `message-mode' are available
+for querying the message information."
+ :type '(choice (const :tag "move message to mu4e-sent-folder" sent)
+ (const :tag "move message to mu4e-trash-folder" trash)
+ (const :tag "delete message" delete))
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-context-policy 'ask
+ "Policy for determining the context when composing a new message.
+
+If the value is `always-ask', ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is used. Otherwise, if none of the
+contexts match, we have the following choices:
+
+- `pick-first': pick the first of the contexts available (ie. the default)
+- `ask': ask the user
+- `ask-if-none': ask if there is no context yet, otherwise leave it as it is
+- nil: return nil; leaves the current context as is.
+
+Also see `mu4e-context-policy'."
+ :type '(choice
+ (const :tag "Always ask what context to use" always-ask)
+ (const :tag "Ask if none of the contexts match" ask)
+ (const :tag "Ask when there's no context yet" ask-if-none)
+ (const :tag "Pick the first context if none match" pick-first)
+ (const :tag "Don't change the context when none match" nil))
+ :safe 'symbolp
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-crypto-policy
+ '(encrypt-encrypted-replies sign-encrypted-replies)
+ "Policy to control when messages will be signed/encrypted.
+
+The value is a list, whose members determine the behaviour of
+`mu4e~compose-crypto-message'. Specifically, it might contain:
+
+- `sign-all-messages': Always add a signature.
+- `sign-new-messages': Add a signature to new message, ie.
+ messages that aren't responses to another message.
+- `sign-forwarded-messages': Add a signature when forwarding
+ a message
+- `sign-edited-messages': Add a signature to drafts
+- `sign-all-replies': Add a signature when responding to
+ another message.
+- `sign-plain-replies': Add a signature when responding to
+ non-encrypted messages.
+- `sign-encrypted-replies': Add a signature when responding
+ to encrypted messages.
+
+It should be noted that certain symbols have priorities over one
+another. So `sign-all-messages' implies `sign-all-replies', which
+in turn implies `sign-plain-replies'. Adding both to the set, is
+not a contradiction, but a redundant configuration.
+
+All `sign-*' options have a `encrypt-*' analogue."
+ :type '(set :greedy t
+ (const :tag "Sign all messages" sign-all-messages)
+ (const :tag "Encrypt all messages" encrypt-all-messages)
+ (const :tag "Sign new messages" sign-new-messages)
+ (const :tag "Encrypt new messages" encrypt-new-messages)
+ (const :tag "Sign forwarded messages" sign-forwarded-messages)
+ (const :tag "Encrypt forwarded messages" encrypt-forwarded-messages)
+ (const :tag "Sign edited messages" sign-edited-messages)
+ (const :tag "Encrypt edited messages" edited-forwarded-messages)
+ (const :tag "Sign all replies" sign-all-replies)
+ (const :tag "Encrypt all replies" encrypt-all-replies)
+ (const :tag "Sign replies to plain messages" sign-plain-replies)
+ (const :tag "Encrypt replies to plain messages" encrypt-plain-replies)
+ (const :tag "Sign replies to encrypted messages" sign-encrypted-replies)
+ (const :tag "Encrypt replies to encrypted messages" encrypt-encrypted-replies))
+ :group 'mu4e-compose)
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-encrypted-policy "The use of the
+ 'mu4e-compose-crypto-reply-encrypted-policy' variable is deprecated.
+ 'mu4e-compose-crypto-policy' should be used instead" "2020-03-06")
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-plain-policy "The use of the
+ 'mu4e-compose-crypto-reply-plain-policy' variable is deprecated.
+ 'mu4e-compose-crypto-policy' should be used instead"
+ "2020-03-06")
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-policy "The use of the
+ 'mu4e-compose-crypto-reply-policy' variable is deprecated.
+ 'mu4e-compose-crypto-reply-plain-policy' and
+ 'mu4e-compose-crypto-reply-encrypted-policy' should be used instead"
+ "2017-09-02")
+
+(defcustom mu4e-compose-format-flowed nil
+ "Whether to compose messages to be sent as format=flowed.
+\(Or with long lines if variable `use-hard-newlines' is set to
+nil). The variable `fill-flowed-encode-column' lets you customize
+the width beyond which format=flowed lines are wrapped."
+ :type 'boolean
+ :safe 'booleanp
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-pre-hook nil
+ "Hook run just *before* message composition starts.
+If the compose-type is a symbol, either `reply' or `forward', the
+variable `mu4e-compose-parent-message' points to the message
+replied to / being forwarded / edited, and `mu4e-compose-type'
+contains the type of message to be composed.
+
+Note that there is no draft message yet when this hook runs, it
+is meant for influencing the how mu4e constructs the draft
+message. If you want to do something with the draft messages after
+it has been constructed, `mu4e-compose-mode-hook' would be the
+place to do that."
+ :type 'hook
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-dont-reply-to-self nil
+ "If non-nil, do not include self.
+Selfness is decided by `mu4e-personal-address-p'"
+ :type 'boolean
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-cite-function
+ (or message-cite-function 'message-cite-original-without-signature)
+ "The function for citing message in replies and forwards.
+This is the mu4e-specific version of
+`message-cite-function'."
+ :type 'function
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-signature
+ (or message-signature "Sent with my mu4e")
+ "The message signature.
+\(i.e. the blob at the bottom of messages). This is the
+mu4e-specific version of `message-signature'."
+ :type '(choice string
+ (const :tag "None" nil)
+ (const :tag "Contents of signature file" t)
+ function sexp)
+ :risky t
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-signature-auto-include t
+ "Whether to automatically include a message-signature."
+ :type 'boolean
+ :group 'mu4e-compose)
+
+(make-obsolete-variable 'mu4e-compose-auto-include-date
+ "This is done unconditionally now" "1.3.5")
+
+(defcustom mu4e-compose-in-new-frame nil
+ "Whether to compose messages in a new frame."
+ :type 'boolean
+ :group 'mu4e-compose)
+
+(defvar mu4e-user-agent-string
+ (format "mu4e %s; emacs %s" mu4e-mu-version emacs-version)
+ "The User-Agent string for mu4e, or nil.")
+
+(defvar mu4e-view-date-format)
+
+(defvar mu4e-compose-type nil
+ "The compose-type for this buffer.
+This is a symbol, `new', `forward', `reply' or `edit'.")
+\f
+
+(defun mu4e~draft-cite-original (msg)
+ "Return a cited version of the original message MSG as a plist.
+This function uses `mu4e-compose-cite-function', and as such all
+its settings apply."
+ (with-temp-buffer
+ (when (fboundp 'mu4e-view-message-text) ;; keep bytecompiler happy
+ (let ((mu4e-view-date-format "%Y-%m-%dT%T%z"))
+ (insert (mu4e-view-message-text msg)))
+ (message-yank-original)
+ (goto-char (point-min))
+ (push-mark (point-max))
+ ;; set the the signature separator to 'loose', since in the real world,
+ ;; many message don't follow the standard...
+ (let ((message-signature-separator "^-- *$")
+ (message-signature-insert-empty-line t))
+ (funcall mu4e-compose-cite-function))
+ (pop-mark)
+ (goto-char (point-min))
+ (buffer-string))))
+
+(defun mu4e~draft-header (hdr val)
+ "Return a header line of the form \"HDR: VAL\".
+If VAL is nil, return nil."
+ ;; note: the propertize here is currently useless, since gnus sets its own
+ ;; later.
+ (when val (format "%s: %s\n"
+ (propertize hdr 'face 'mu4e-header-key-face)
+ (propertize val 'face 'mu4e-header-value-face))))
+
+(defconst mu4e~max-reference-num 21
+ "Specifies the maximum number of References:.
+As suggested by `message-shorten-references'.")
+
+(defun mu4e~shorten-1 (list cut surplus)
+ "Cut SURPLUS elements out of LIST.
+Beginning with CUTth
+one. Code borrowed from `message-shorten-1'."
+ (setcdr (nthcdr (- cut 2) list)
+ (nthcdr (+ (- cut 2) surplus 1) list)))
+
+
+(defun mu4e~fontify-signature ()
+ "Give the message signatures a distinctive color. This is used
+in the view and compose modes and will color each signature in
+digest messages adhering to RFC 1153."
+ (let ((inhibit-read-only t))
+ (save-excursion
+ ;; give the footer a different color...
+ (goto-char (point-min))
+ (while (re-search-forward "^-- *$" nil t)
+ (let ((p (point))
+ (end (or ;; 30 by RFC1153
+ (re-search-forward "\\(^-\\{30\\}.*$\\)" nil t)
+ (point-max))))
+ (add-text-properties p end '(face mu4e-footer-face)))))))
+
+(defun mu4e~draft-references-construct (msg)
+ "Construct the value of the References: header based on MSG.
+This assumes a comma-separated string. Normally, this the concatenation of the
+existing References + In-Reply-To (which may be empty, an note
+that :references includes the old in-reply-to as well) and the
+message-id. If the message-id is empty, returns the old
+References. If both are empty, return nil."
+ (let* ( ;; these are the ones from the message being replied to / forwarded
+ (refs (mu4e-message-field msg :references))
+ (msgid (mu4e-message-field msg :message-id))
+ ;; now, append in
+ (refs (if (and msgid (not (string= msgid "")))
+ (append refs (list msgid)) refs))
+ ;; no doubles
+ (refs (cl-delete-duplicates refs :test #'equal))
+ (refnum (length refs))
+ (cut 2))
+ ;; remove some refs when there are too many
+ (when (> refnum mu4e~max-reference-num)
+ (let ((surplus (- refnum mu4e~max-reference-num)))
+ (mu4e~shorten-1 refs cut surplus)))
+ (mapconcat (lambda (id) (format "<%s>" id)) refs " ")))
+
+\f
+;;; Determine the recipient fields for new messages
+
+(defun mu4e~draft-recipients-list-to-string (lst)
+ "Convert a lst LST of address cells into a string.
+This is specified as a comma-separated list of e-mail addresses.
+If LST is nil, returns nil."
+ (when lst
+ (mapconcat
+ (lambda (contact) (mu4e-contact-full contact)) lst ", ")))
+
+(defun mu4e~draft-address-cell-equal (cell1 cell2)
+ "Return t if CELL1 and CELL2 have the same e-mail address.
+The comparison is done case-insensitively. If the cells done
+match return nil. CELL1 and CELL2 are cons cells of the
+form (NAME . EMAIL)."
+ (string=
+ (downcase (or (mu4e-contact-email cell1) ""))
+ (downcase (or (mu4e-contact-email cell2) ""))))
+
+
+(defun mu4e~draft-create-to-lst (origmsg)
+ "Create a list of address for the To: in a new message.
+This is based on the original message ORIGMSG. If the Reply-To
+address is set, use that, otherwise use the From address. Note,
+whatever was in the To: field before, goes to the Cc:-list (if
+we're doing a reply-to-all). Special case: if we were the sender
+of the original, we simple copy the list form the original."
+ (let ((reply-to
+ (or (plist-get origmsg :reply-to) (plist-get origmsg :from))))
+ (cl-delete-duplicates reply-to :test #'mu4e~draft-address-cell-equal)
+ (if mu4e-compose-dont-reply-to-self
+ (cl-delete-if
+ (lambda (to-cell)
+ (mu4e-personal-address-p (mu4e-contact-email to-cell)))
+ reply-to)
+ reply-to)))
+
+
+(defun mu4e~strip-ignored-addresses (addrs)
+ "Return all addresses that are not to be ignored.
+I.e. return all the addresses in ADDRS not matching
+`mu4e-compose-reply-ignore-address'."
+ (cond
+ ((null mu4e-compose-reply-ignore-address)
+ addrs)
+ ((functionp mu4e-compose-reply-ignore-address)
+ (cl-remove-if
+ (lambda (elt)
+ (funcall mu4e-compose-reply-ignore-address (mu4e-contact-email elt)))
+ addrs))
+ (t
+ ;; regexp or list of regexps
+ (let* ((regexp mu4e-compose-reply-ignore-address)
+ (regexp (if (listp regexp)
+ (mapconcat (lambda (elt) (concat "\\(" elt "\\)"))
+ regexp "\\|")
+ regexp)))
+ (cl-remove-if
+ (lambda (elt)
+ (string-match regexp (mu4e-contact-email elt)))
+ addrs)))))
+
+(defun mu4e~draft-create-cc-lst (origmsg &optional reply-all include-from)
+ "Create a list of address for the Cc: in a new message.
+This is based on the original message ORIGMSG, and whether it's a
+REPLY-ALL."
+ (when reply-all
+ (let* ((cc-lst ;; get the cc-field from the original, remove dups
+ (cl-delete-duplicates
+ (append
+ (plist-get origmsg :to)
+ (plist-get origmsg :cc)
+ (when include-from(plist-get origmsg :from))
+ (plist-get origmsg :list-post))
+ :test #'mu4e~draft-address-cell-equal))
+ ;; now we have the basic list, but we must remove
+ ;; addresses also in the To: list
+ (cc-lst
+ (cl-delete-if
+ (lambda (cc-cell)
+ (cl-find-if
+ (lambda (to-cell)
+ (mu4e~draft-address-cell-equal cc-cell to-cell))
+ (mu4e~draft-create-to-lst origmsg)))
+ cc-lst))
+ ;; remove ignored addresses
+ (cc-lst (mu4e~strip-ignored-addresses cc-lst))
+ ;; finally, we need to remove ourselves from the cc-list
+ ;; unless mu4e-compose-keep-self-cc is non-nil
+ (cc-lst
+ (if (or mu4e-compose-keep-self-cc (null user-mail-address))
+ cc-lst
+ (cl-delete-if
+ (lambda (cc-cell)
+ (mu4e-personal-address-p (mu4e-contact-email cc-cell)))
+ cc-lst))))
+ cc-lst)))
+
+(defun mu4e~draft-recipients-construct (field origmsg &optional reply-all include-from)
+ "Create value (a string) for the recipient FIELD.
+\(which is a symbol, :to or :cc), based on the original message ORIGMSG,
+and (optionally) REPLY-ALL which indicates this is a reply-to-all
+message. Return nil if there are no recipients for the particular field."
+ (mu4e~draft-recipients-list-to-string
+ (cl-case field
+ (:to
+ (mu4e~draft-create-to-lst origmsg))
+ (:cc
+ (mu4e~draft-create-cc-lst origmsg reply-all include-from))
+ (otherwise
+ (mu4e-error "Unsupported field")))))
+
+(defun mu4e~draft-from-construct ()
+ "Construct a value for the From:-field of the reply.
+This is based on the variable `user-full-name' and
+`user-mail-address'; if the latter is nil, function returns nil."
+ (when user-mail-address
+ (mu4e-contact-full (mu4e-contact-make
+ user-full-name
+ user-mail-address))))
+
+;;; Header separators
+
+(defun mu4e~draft-insert-mail-header-separator ()
+ "Insert `mail-header-separator' in the first empty line of the message.
+`message-mode' needs this line to know where the headers end and
+the body starts. Note, in `mu4e-compose-mode', we use
+`before-save-hook' and `after-save-hook' to ensure that this
+separator is never written to the message file. Also see
+`mu4e-remove-mail-header-separator'."
+ ;; we set this here explicitly, since (as it has happened) a wrong
+ ;; value for this (such as "") breaks address completion and other things
+ (set (make-local-variable 'mail-header-separator) "--text follows this line--")
+ (put 'mail-header-separator 'permanent-local t)
+ (save-excursion
+ ;; make sure there's not one already
+ (mu4e~draft-remove-mail-header-separator)
+ (let ((sepa (propertize mail-header-separator
+ 'intangible t
+ ;; don't make this read-only, message-mode
+ ;; seems to require it being writable in some cases
+ ;;'read-only "Can't touch this"
+ 'rear-nonsticky t
+ 'font-lock-face 'mu4e-compose-separator-face)))
+ (widen)
+ ;; search for the first empty line
+ (goto-char (point-min))
+ (if (search-forward-regexp "^$" nil t)
+ (progn
+ (replace-match sepa)
+ ;; `message-narrow-to-headers` searches for a
+ ;; `mail-header-separator` followed by a new line. Therefore, we
+ ;; must insert a newline if on the last line of the buffer.
+ (when (= (point) (point-max))
+ (insert "\n")))
+ (progn ;; no empty line? then prepend one
+ (goto-char (point-max))
+ (insert "\n" sepa))))))
+
+(defun mu4e~draft-remove-mail-header-separator ()
+ "Remove `mail-header-separator'.
+We do this before saving a
+file (and restore it afterwards), to ensure that the separator
+never hits the disk. Also see
+`mu4e~draft-insert-mail-header-separator."
+ (save-excursion
+ (widen)
+ (goto-char (point-min))
+ ;; remove the --text follows this line-- separator
+ (when (search-forward-regexp (concat "^" mail-header-separator) nil t)
+ (let ((inhibit-read-only t))
+ (replace-match "")))))
+
+(defun mu4e~draft-reply-all-p (origmsg)
+ "Ask user whether she wants to reply to *all* recipients.
+If there is just one recipient of ORIGMSG do nothing."
+ (let* ((recipnum
+ (+ (length (mu4e~draft-create-to-lst origmsg))
+ (length (mu4e~draft-create-cc-lst origmsg t))))
+ (response
+ (if (< recipnum 2)
+ 'all ;; with less than 2 recipients, we can reply to 'all'
+ (mu4e-read-option
+ "Reply to "
+ `( (,(format "all %d recipients" recipnum) . all)
+ ("sender only" . sender-only))))))
+ (eq response 'all)))
+
+(defun mu4e~draft-message-filename-construct (&optional flagstr)
+ "Construct a randomized name for a message file with flags FLAGSTR.
+It looks something like
+ <time>-<random>.<hostname>:2,
+You can append flags."
+ (let* ((sysname (if (fboundp 'system-name)
+ (system-name)
+ (with-no-warnings system-name)))
+ (sysname (if (string= sysname "") "localhost" sysname))
+ (hostname (downcase
+ (save-match-data
+ (substring sysname
+ (string-match "^[^.]+" sysname)
+ (match-end 0))))))
+ (format "%s.%04x%04x%04x%04x.%s%s2,%s"
+ (format-time-string "%s" (current-time))
+ (random 65535) (random 65535) (random 65535) (random 65535)
+ hostname mu4e-maildir-info-delimiter (or flagstr ""))))
+
+(defun mu4e~draft-common-construct ()
+ "Construct the common headers for each message."
+ (concat
+ (when-let ((organization (message-make-organization)))
+ (mu4e~draft-header "Organization" organization))
+ (when mu4e-user-agent-string
+ (mu4e~draft-header "User-agent" mu4e-user-agent-string))
+ (mu4e~draft-header "Date" (message-make-date))))
+
+(defconst mu4e~draft-reply-prefix "Re: "
+ "String to prefix replies with.")
+
+(defun mu4e~draft-reply-construct-recipients (origmsg)
+ "Determine the to/cc recipients for a reply message."
+ (let* ((reply-to-self (mu4e-message-contact-field-matches-me origmsg :from))
+ ;; reply-to-self implies reply-all
+ (reply-all (or reply-to-self
+ (eq mu4e-compose-reply-recipients 'all)
+ (and (not (eq mu4e-compose-reply-recipients 'sender))
+ (mu4e~draft-reply-all-p origmsg)))))
+ (concat
+ (if reply-to-self
+ ;; When we're replying to ourselves, simply keep the same headers.
+ (concat
+ (mu4e~draft-header "To" (mu4e~draft-recipients-list-to-string
+ (mu4e-message-field origmsg :to)))
+ (mu4e~draft-header "Cc" (mu4e~draft-recipients-list-to-string
+ (mu4e-message-field origmsg :cc))))
+
+ ;; if there's no-one in To, copy the CC-list
+ (if (zerop (length (mu4e~draft-create-to-lst origmsg)))
+ (mu4e~draft-header "To" (mu4e~draft-recipients-construct
+ :cc origmsg reply-all))
+ ;; otherwise...
+ (concat
+ (mu4e~draft-header "To" (mu4e~draft-recipients-construct :to origmsg))
+ (mu4e~draft-header "Cc" (mu4e~draft-recipients-construct :cc origmsg reply-all))))))))
+
+(defun mu4e~draft-reply-construct-recipients-list (origmsg)
+ "Determine the to/cc recipients for a reply message to a
+mailing-list."
+ (let* ( ;; reply-to-self implies reply-all
+ (list-post (plist-get origmsg :list-post))
+ (from (plist-get origmsg :from))
+ (recipnum
+ (+ (length (mu4e~draft-create-to-lst origmsg))
+ (length (mu4e~draft-create-cc-lst origmsg t t))))
+ (sender (mu4e-contact-full (car from)))
+ (reply-type
+ (mu4e-read-option
+ "Reply to mailing-list "
+ `( (,(format "all %d recipient(s)" recipnum) . all)
+ (,(format "list-only (%s)" (cdar list-post)) . list-only)
+ (,(format "sender-only (%s)" sender) . sender-only)))))
+ (cl-case reply-type
+ (all
+ (concat
+ (mu4e~draft-header "To" (mu4e~draft-recipients-construct :to origmsg))
+ (mu4e~draft-header "Cc" (mu4e~draft-recipients-construct :cc origmsg t t))))
+ (list-only
+ (mu4e~draft-header "To"
+ (mu4e~draft-recipients-list-to-string list-post)))
+ (sender-only
+ (mu4e~draft-header "To"
+ (mu4e~draft-recipients-list-to-string from))))))
+
+(defun mu4e~draft-reply-construct (origmsg)
+ "Create a draft message as a reply to ORIGMSG.
+Replying-to-self is special; in that case, the To and Cc fields
+will be the same as in the original."
+ (let* ((old-msgid (plist-get origmsg :message-id))
+ (subject (concat mu4e~draft-reply-prefix
+ (message-strip-subject-re
+ (or (plist-get origmsg :subject) ""))))
+ (list-post (plist-get origmsg :list-post)))
+ (concat
+ (mu4e~draft-header "From" (or (mu4e~draft-from-construct) ""))
+ (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address)
+
+ (if list-post ;; mailing-lists are a bit special.
+ (mu4e~draft-reply-construct-recipients-list origmsg)
+ (mu4e~draft-reply-construct-recipients origmsg))
+
+ (mu4e~draft-header "Subject" subject)
+ (mu4e~draft-header "References"
+ (mu4e~draft-references-construct origmsg))
+ (mu4e~draft-common-construct)
+ (when old-msgid
+ (mu4e~draft-header "In-reply-to" (format "<%s>" old-msgid)))
+ "\n\n"
+ (mu4e~draft-cite-original origmsg))))
+
+(defconst mu4e~draft-forward-prefix "Fwd: "
+ "String to prefix replies with.")
+
+(defun mu4e~draft-forward-construct (origmsg)
+ "Create a draft forward message for original message ORIGMSG."
+ (let ((subject
+ (or (plist-get origmsg :subject) "")))
+ (concat
+ (mu4e~draft-header "From" (or (mu4e~draft-from-construct) ""))
+ (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address)
+ (mu4e~draft-header "To" "")
+ (mu4e~draft-common-construct)
+ (mu4e~draft-header "References"
+ (mu4e~draft-references-construct origmsg))
+ (mu4e~draft-header "Subject"
+ (concat
+ ;; if there's no Fwd: yet, prepend it
+ (if (string-match "^Fwd:" subject)
+ ""
+ mu4e~draft-forward-prefix)
+ subject))
+ (unless mu4e-compose-forward-as-attachment
+ (concat
+ "\n\n"
+ (mu4e~draft-cite-original origmsg))))))
+
+(defun mu4e~draft-newmsg-construct ()
+ "Create a new message."
+ (concat
+ (mu4e~draft-header "From" (or (mu4e~draft-from-construct) ""))
+ (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address)
+ (mu4e~draft-header "To" "")
+ (mu4e~draft-header "Subject" "")
+ (mu4e~draft-common-construct)))
+
+(defvar mu4e~draft-drafts-folder nil
+ "The drafts-folder for this compose buffer.
+This is based on `mu4e-drafts-folder', which is evaluated once.")
+
+(defun mu4e~draft-open-file (path switch-function)
+ "Open the the draft file at PATH."
+ (let ((buf (find-file-noselect path)))
+ (funcall (or
+ switch-function
+ (and mu4e-compose-in-new-frame 'switch-to-buffer-other-frame)
+ 'switch-to-buffer)
+ buf)))
+
+
+(defun mu4e~draft-determine-path (draft-dir)
+ "Determines the path for a new draft file in DRAFT-DIR."
+ (format "%s/%s/cur/%s"
+ (mu4e-root-maildir) draft-dir (mu4e~draft-message-filename-construct "DS")))
+
+
+(defun mu4e-draft-open (compose-type &optional msg switch-function)
+ "Open a draft file for a message MSG.
+In case of a new message (when COMPOSE-TYPE is `reply', `forward'
+ or `new'), open an existing draft (when COMPOSE-TYPE is `edit'),
+ or re-send an existing message (when COMPOSE-TYPE is `resend').
+
+The name of the draft folder is constructed from the
+concatenation of `(mu4e-root-maildir)' and `mu4e-drafts-folder' (the
+latter will be evaluated). The message file name is a unique name
+determined by `mu4e-send-draft-file-name'. The initial contents
+will be created from either `mu4e~draft-reply-construct', or
+`mu4e~draft-forward-construct' or `mu4e~draft-newmsg-construct'."
+ (let ((draft-dir nil))
+ (cl-case compose-type
+
+ (edit
+ ;; case-1: re-editing a draft messages. in this case, we do know the
+ ;; full path, but we cannot really know 'drafts folder'... we make a
+ ;; guess
+ (setq draft-dir (mu4e--guess-maildir (mu4e-message-field msg :path)))
+ (mu4e~draft-open-file (mu4e-message-field msg :path) switch-function))
+
+ (resend
+ ;; case-2: copy some exisisting message to a draft message, then edit
+ ;; that.
+ (setq draft-dir (mu4e--guess-maildir (mu4e-message-field msg :path)))
+ (let ((draft-path (mu4e~draft-determine-path draft-dir)))
+ (copy-file (mu4e-message-field msg :path) draft-path)
+ (mu4e~draft-open-file draft-path switch-function)))
+
+ ((reply forward new)
+ ;; case-3: creating a new message; in this case, we can determine
+ ;; mu4e-get-drafts-folder
+ (setq draft-dir (mu4e-get-drafts-folder msg))
+ (let ((draft-path (mu4e~draft-determine-path draft-dir))
+ (initial-contents
+ (cl-case compose-type
+ (reply (mu4e~draft-reply-construct msg))
+ (forward (mu4e~draft-forward-construct msg))
+ (new (mu4e~draft-newmsg-construct)))))
+ (mu4e~draft-open-file draft-path switch-function)
+ (insert initial-contents)
+ (newline)
+ ;; include the message signature (if it's set)
+ (if (and mu4e-compose-signature-auto-include mu4e-compose-signature)
+ (let ((message-signature mu4e-compose-signature))
+ (save-excursion
+ (message-insert-signature)
+ (mu4e~fontify-signature))))))
+ (t (mu4e-error "Unsupported compose-type %S" compose-type)))
+ ;; if we didn't find a draft folder yet, try some default
+ (unless draft-dir
+ (setq draft-dir (mu4e-get-drafts-folder msg)))
+ ;; evaluate mu4e~drafts-drafts-folder once, here, and use that value
+ ;; throughout.
+ (set (make-local-variable 'mu4e~draft-drafts-folder) draft-dir)
+ (put 'mu4e~draft-drafts-folder 'permanent-local t)
+ (unless mu4e~draft-drafts-folder
+ (mu4e-error "Failed to determine drafts folder"))))
+
+;;; _
+(provide 'mu4e-draft)
+;;; mu4e-draft.el ends here
--- /dev/null
+;;; mu4e-folders.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Dealing with maildirs & folders
+
+;;; Code:
+(require 'mu4e-helpers)
+(require 'mu4e-context)
+(require 'mu4e-server)
+\f
+;;; Customization
+(defgroup mu4e-folders nil
+ "Special folders."
+ :group 'mu4e)
+
+(defcustom mu4e-drafts-folder "/drafts"
+ "Folder for draft messages, relative to the root maildir.
+For instance, \"/drafts\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note, the message
+parameter refers to the original message being replied to / being
+forwarded / re-edited and is nil otherwise. `mu4e-drafts-folder'
+is only evaluated once."
+ :type '(choice
+ (string :tag "Folder name")
+ (function :tag "Function return folder name"))
+ :group 'mu4e-folders)
+
+(defcustom mu4e-refile-folder "/archive"
+ "Folder for refiling messages, relative to the root maildir.
+For instance \"/Archive\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note that the
+message parameter refers to the message-at-point."
+ :type '(choice
+ (string :tag "Folder name")
+ (function :tag "Function return folder name"))
+ :group 'mu4e-folders)
+
+(defcustom mu4e-sent-folder "/sent"
+ "Folder for sent messages, relative to the root maildir.
+For instance, \"/Sent Items\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note that the
+message parameter refers to the original message being replied to
+/ being forwarded / re-edited, and is nil otherwise."
+ :type '(choice
+ (string :tag "Folder name")
+ (function :tag "Function return folder name"))
+ :group 'mu4e-folders)
+
+(defcustom mu4e-trash-folder "/trash"
+ "Folder for trashed messages, relative to the root maildir.
+For instance, \"/trash\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. When using
+`mu4e-trash-folder' in the headers view (when marking messages
+for trash). Note that the message parameter refers to the
+message-at-point. When using it when composing a message (see
+`mu4e-sent-messages-behavior'), this refers to the original
+message being replied to / being forwarded / re-edited, and is
+nil otherwise."
+ :type '(choice
+ (string :tag "Folder name")
+ (function :tag "Function return folder name"))
+ :group 'mu4e-folders)
+
+(defcustom mu4e-maildir-shortcuts nil
+ "A list of maildir shortcuts.
+This makes it possible to quickly go to a particular
+maildir (folder), or quickly moving messages to them (e.g., for
+archiving or refiling).
+
+Each of the list elements is a plist with at least:
+`:maildir' - the maildir for the shortcut (e.g. \"/archive\")
+`:key' - the shortcut key.
+
+Optionally, you can add the following:
+`:hide' - if t, the shortcut is hidden from the main-view and
+speedbar.
+`:hide-unread' - do not show the counts of unread/total number
+ of matches for the maildir in the main-view, and is implied
+from `:hide'.
+
+For backward compatibility, an older form is recognized as well:
+
+ (maildir . key), where MAILDIR is a maildir (such as
+\"/archive/\"), and key is a single character.
+
+You can use these shortcuts in the headers and view buffers, for
+example with `mu4e-mark-for-move-quick' (or \"m\", by default) or
+`mu4e-jump-to-maildir' (or \"j\", by default), followed by the
+designated shortcut character for the maildir.
+
+Unlike in search queries, folder names with spaces in them must
+NOT be quoted, since mu4e does this for you."
+ :type '(repeat (cons (string :tag "Maildir") character))
+ :version "1.3.9"
+ :group 'mu4e-folders)
+
+(defcustom mu4e-maildir-info-delimiter
+ (if (member system-type '(ms-dos windows-nt cygwin))
+ ";" ":")
+ "Separator character between message identifier and flags.
+It defaults to ':' on most platforms, except on Windows, where it
+is not allowed and we use ';' for compatibility with mbsync,
+offlineimap and other programs."
+ :type 'string
+ :group 'mu4e-folders)
+
+(defcustom mu4e-attachment-dir (expand-file-name "~/")
+ "Default directory for attaching and saving attachments.
+
+This can be either a string (a file system path), or a function
+that takes a filename and the mime-type as arguments, and returns
+the attachment dir. See Info node `(mu4e) Attachments' for
+details.
+
+When this called for composing a message, both filename and
+mime-type are nill."
+ :type 'directory
+ :group 'mu4e-folders
+ :safe 'stringp)
+
+\f
+(defun mu4e-maildir-shortcuts ()
+ "Get `mu4e-maildir-shortcuts' in the (new) format.
+Converts from the old format if needed."
+ (seq-map (lambda (item) ;; convert from old format?
+ (if (and (consp item) (not (consp (cdr item))))
+ `(:maildir ,(car item) :key ,(cdr item))
+ item))
+ mu4e-maildir-shortcuts))
+
+(defun mu4e--maildirs-with-query ()
+ "Like `mu4e-maildir-shortcuts', but with :query populated.
+This is compatibile with `mu4e-bookmarks'."
+ (seq-map
+ (lambda (item)
+ (let* ((maildir (plist-get item :maildir))
+ (item (plist-put item :name maildir))
+ (item (plist-put item :query (format "maildir:\"%s\"" maildir))))
+ item)) ;; we don't need ":maildir", but it's harmless.
+ (mu4e-maildir-shortcuts)))
+
+;; the standard folders can be functions too
+(defun mu4e--get-folder (foldervar msg)
+ "Within the mu-context of MSG, get message folder FOLDERVAR.
+If FOLDER is a string, return it, if it is a function, evaluate
+this function with MSG as parameter which may be nil, and return
+the result."
+ (unless (member foldervar
+ '(mu4e-sent-folder mu4e-drafts-folder
+ mu4e-trash-folder mu4e-refile-folder))
+ (mu4e-error "Folder must be one of mu4e-(sent|drafts|trash|refile)-folder"))
+ ;; get the value with the vars for the relevants context let-bound
+ (with-mu4e-context-vars (mu4e-context-determine msg nil)
+ (let* ((folder (symbol-value foldervar))
+ (val
+ (cond
+ ((stringp folder) folder)
+ ((functionp folder) (funcall folder msg))
+ (t (mu4e-error "Unsupported type for %S" folder)))))
+ (or val (mu4e-error "%S evaluates to nil" foldervar)))))
+
+(defun mu4e-get-drafts-folder (&optional msg)
+ "Get the sent folder, optionallly based on MSG.
+See `mu4e-drafts-folder'." (mu4e--get-folder 'mu4e-drafts-folder msg))
+
+(defun mu4e-get-refile-folder (&optional msg)
+ "Get the folder for refiling, optionallly based on MSG.
+See `mu4e-refile-folder'." (mu4e--get-folder 'mu4e-refile-folder msg))
+
+(defun mu4e-get-sent-folder (&optional msg)
+ "Get the sent folder, optionallly based on MSG.
+See `mu4e-sent-folder'." (mu4e--get-folder 'mu4e-sent-folder msg))
+
+(defun mu4e-get-trash-folder (&optional msg)
+ "Get the sent folder, optionallly based on MSG.
+See `mu4e-trash-folder'." (mu4e--get-folder 'mu4e-trash-folder msg))
+
+
+;;; Maildirs
+(defun mu4e--guess-maildir (path)
+ "Guess the maildir for PATH, or nil if cannot find it."
+ (let ((idx (string-match (mu4e-root-maildir) path)))
+ (when (and idx (zerop idx))
+ (replace-regexp-in-string
+ (mu4e-root-maildir)
+ ""
+ (expand-file-name
+ (concat path "/../.."))))))
+
+(defun mu4e-create-maildir-maybe (dir)
+ "Offer to create maildir DIR if it does not exist yet.
+Return t if the dir already existed, or an attempt has been made to
+create it -- we cannot be sure creation succeeded here, since this
+is done asynchronously. Otherwise, return nil. NOte, DIR has to be
+an absolute path."
+ (if (and (file-exists-p dir) (not (file-directory-p dir)))
+ (mu4e-error "File %s exists, but is not a directory" dir))
+ (cond
+ ((file-directory-p dir) t)
+ ((yes-or-no-p (mu4e-format "%s does not exist yet. Create now?" dir))
+ (mu4e--server-mkdir dir) t)
+ (t nil)))
+
+(defun mu4e~get-maildirs-1 (path mdir)
+ "Get maildirs for MDIR under PATH.
+Do so recursively and produce a list of relative paths."
+ (let ((dirs)
+ (dentries
+ (ignore-errors
+ (directory-files-and-attributes
+ (concat path mdir) nil
+ "^[^.]\\|\\.[^.][^.]" t))))
+ (dolist (dentry dentries)
+ (when (and (booleanp (cadr dentry)) (cadr dentry))
+ (if (file-accessible-directory-p
+ (concat (mu4e-root-maildir) "/" mdir "/" (car dentry) "/cur"))
+ (setq dirs (cons (concat mdir (car dentry)) dirs)))
+ (unless (member (car dentry) '("cur" "new" "tmp"))
+ (setq dirs
+ (append dirs
+ (mu4e~get-maildirs-1 path
+ (concat mdir
+ (car dentry) "/")))))))
+ dirs))
+
+(defvar mu4e-cache-maildir-list nil
+ "Whether to cache the list of maildirs.
+Set it to t if you find that generating the list on the fly is
+too slow. If you do, you can set `mu4e-maildir-list' to nil to
+force regenerating the cache the next time `mu4e-get-maildirs'
+gets called.")
+
+(defvar mu4e-maildir-list nil
+ "Cached list of maildirs.")
+
+(defun mu4e-get-maildirs ()
+ "Get maildirs under `mu4e-maildir'.
+Do so recursively, and produce a list of relative paths (ie.,
+/archive, /sent etc.). Most of the work is done in
+`mu4e~get-maildirs-1'. Note, these results are /cached/ if
+`mu4e-cache-maildir-list' is customized to non-nil. In that case,
+the list of maildirs will not change until you restart mu4e."
+ (unless (and mu4e-maildir-list mu4e-cache-maildir-list)
+ (setq mu4e-maildir-list
+ (sort
+ (append
+ (when (file-accessible-directory-p
+ (concat (mu4e-root-maildir) "/cur")) '("/"))
+ (mu4e~get-maildirs-1 (mu4e-root-maildir) "/"))
+ (lambda (s1 s2) (string< (downcase s1) (downcase s2))))))
+ mu4e-maildir-list)
+
+(defun mu4e-ask-maildir (prompt)
+ "Ask the user for a shortcut (using PROMPT).
+As per (mu4e-maildir-shortcuts), then return the corresponding
+folder name. If the special shortcut \"o\" (for _o_ther) is used,
+or if (mu4e-maildir-shortcuts) evaluates to nil, let user choose
+from all maildirs under `mu4e-maildir'."
+ (let ((prompt (mu4e-format "%s" prompt)))
+ (if (not (mu4e-maildir-shortcuts))
+ (substring-no-properties
+ (funcall mu4e-completing-read-function prompt (mu4e-get-maildirs)))
+ (let* ((mlist (append (mu4e-maildir-shortcuts)
+ '((:maildir "ther" :key ?o))))
+ (fnames
+ (mapconcat
+ (lambda (item)
+ (concat
+ "["
+ (propertize (make-string 1 (plist-get item :key))
+ 'face 'mu4e-highlight-face)
+ "]"
+ (plist-get item :maildir)))
+ mlist ", "))
+ (kar (read-char (concat prompt fnames))))
+ (if (member kar '(?/ ?o)) ;; user chose 'other'?
+ (substring-no-properties
+ (funcall mu4e-completing-read-function prompt
+ (mu4e-get-maildirs) nil nil "/"))
+ (or (plist-get
+ (seq-find (lambda (item) (= kar (plist-get item :key)))
+ (mu4e-maildir-shortcuts)) :maildir)
+ (mu4e-warn "Unknown shortcut '%c'" kar)))))))
+
+(defun mu4e-ask-maildir-check-exists (prompt)
+ "Like `mu4e-ask-maildir', PROMPT for existence of the maildir.
+Offer to create it if it does not exist yet."
+ (let* ((mdir (mu4e-ask-maildir prompt))
+ (fullpath (concat (mu4e-root-maildir) mdir)))
+ (unless (file-directory-p fullpath)
+ (and (yes-or-no-p
+ (mu4e-format "%s does not exist. Create now?" fullpath))
+ (mu4e--server-mkdir fullpath)))
+ mdir))
+
+;; mu4e-attachment-dir is either a string or a function that takes a
+;; filename and the mime-type as argument, either (or both) which can
+;; be nil
+
+(defun mu4e~get-attachment-dir (&optional fname mimetype)
+ "Get the directory for saving attachments from `mu4e-attachment-dir'.
+This is optionally based on the file-name FNAME and its MIMETYPE."
+ (let ((dir
+ (cond
+ ((stringp mu4e-attachment-dir)
+ mu4e-attachment-dir)
+ ((functionp mu4e-attachment-dir)
+ (funcall mu4e-attachment-dir fname mimetype))
+ (t
+ (mu4e-error "Unsupported type for mu4e-attachment-dir" )))))
+ (if dir
+ (expand-file-name dir)
+ (mu4e-error "Mu4e-attachment-dir evaluates to nil"))))
+
+(provide 'mu4e-folders)
+;;; mu4e-folders.el ends here
--- /dev/null
+;;; mu4e-headers.el -- part of mu4e -*- lexical-binding: t; coding:utf-8 -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file are function related mu4e-headers-mode, to creating the list of
+;; one-line descriptions of emails, aka 'headers' (not to be confused with
+;; headers like 'To:' or 'Subject:')
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'fringe)
+(require 'hl-line)
+(require 'mailcap)
+(require 'mule-util) ;; seems _some_ people need this for truncate-string-ellipsis
+
+(require 'mu4e-update)
+
+ ;; utility functions
+(require 'mu4e-server)
+(require 'mu4e-vars)
+(require 'mu4e-mark)
+(require 'mu4e-context)
+(require 'mu4e-contacts)
+(require 'mu4e-search)
+(require 'mu4e-compose)
+(require 'mu4e-actions)
+(require 'mu4e-message)
+(require 'mu4e-lists)
+(require 'mu4e-update)
+(require 'mu4e-folders)
+
+(declare-function mu4e-view "mu4e-view")
+(declare-function mu4e--main-view "mu4e-main")
+
+\f
+
+;;; Configuration
+
+(defgroup mu4e-headers nil
+ "Settings for the headers view."
+ :group 'mu4e)
+
+(defcustom mu4e-headers-fields
+ '( (:human-date . 12)
+ (:flags . 6)
+ (:mailing-list . 10)
+ (:from . 22)
+ (:subject . nil))
+ "A list of header fields to show in the headers buffer.
+Each element has the form (HEADER . WIDTH), where HEADER is one of
+the available headers (see `mu4e-header-info') and WIDTH is the
+respective width in characters.
+
+A width of nil means \"unrestricted\", and this is best reserved
+for the rightmost (last) field. Note that emacs may become very
+slow with excessively long lines (1000s of characters), so if you
+regularly get such messages, you want to avoid fields with nil
+altogether."
+ :type `(repeat (cons (choice ,@(mapcar (lambda (h)
+ (list 'const :tag
+ (plist-get (cdr h) :help)
+ (car h)))
+ mu4e-header-info))
+ (choice (integer :tag "width")
+ (const :tag "unrestricted width" nil))))
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-date-format "%x"
+ "Date format to use in the headers view.
+In the format of `format-time-string'."
+ :type 'string
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-time-format "%X"
+ "Time format to use in the headers view.
+In the format of `format-time-string'."
+ :type 'string
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-long-date-format "%c"
+ "Date format to use in the headers view tooltip.
+In the format of `format-time-string'."
+ :type 'string
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-visible-lines 10
+ "Number of lines to display in the header view when using the
+horizontal split-view. This includes the header-line at the top,
+and the mode-line."
+ :type 'integer
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-visible-columns 30
+ "Number of columns to display for the header view when using the
+vertical split-view."
+ :type 'integer
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-precise-alignment nil
+ "When set, use precise (but relatively slow) alignment for columns.
+By default, do it in a slightly inaccurate but faster way. To get
+an idea about the difference, In some tests, the rendering time
+was around 5.8 ms per messages for precise alignment, versus 3.3
+for non-precise aligment (for 445 messages)."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-auto-update t
+ "Whether to automatically update the current headers buffer if an
+indexing operation showed changes."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-advance-after-mark t
+ "With this option set to non-nil, automatically advance to the
+next mail after marking a message in header view."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+
+(defvar mu4e-headers-hide-predicate nil
+ "Predicate function to hide matching heasders.
+If the function evaluates to non-nil when applied a a message
+plist, do not show the corresponding header. The function takes
+one parameter MSG, which is the message plist for the message to
+be hidden or not.
+
+Example that hides all trashed messages:
+
+ (setq mu4e-headers-hide-predicate
+ (lambda (msg)
+ (member \='trashed (mu4e-message-field msg :flags)))).")
+
+(defcustom mu4e-headers-visible-flags
+ '(draft flagged new passed replied trashed attach encrypted signed list personal)
+ "An ordered list of flags to show in the headers buffer.
+Each element is a symbol in the list.
+
+By default, we leave out `unread' and `seen', since those are
+mostly covered by `new', and the display gets cluttered otherwise."
+ :type '(set
+ (const :tag "Draft" draft)
+ (const :tag "Flagged" flagged)
+ (const :tag "New" new)
+ (const :tag "Passed" passed)
+ (const :tag "Replied" replied)
+ (const :tag "Seen" seen)
+ (const :tag "Trashed" trashed)
+ (const :tag "Attach" attach)
+ (const :tag "Encrypted" encrypted)
+ (const :tag "Signed" signed)
+ (const :tag "List" list)
+ (const :tag "Personal" personal)
+ (const :tag "Calendar" calendar))
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-found-hook nil
+ "Hook run just *after* all of the headers for the last search
+query have been received and are displayed."
+ :type 'hook
+ :group 'mu4e-headers)
+
+;;; Public variables
+
+(defvar mu4e-headers-sort-field :date
+ "Field to sort the headers by. A symbol:
+one of: `:date', `:subject', `:size', `:prio', `:from', `:to.',
+`:list'.
+
+Note that when threading is enabled (through
+`mu4e-search-threads'), the headers are exclusively sorted
+chronologically (`:date') by the newest message in the thread.")
+
+(defvar mu4e-headers-sort-direction 'descending
+ "Direction to sort by; a symbol either `descending' (sorting
+ Z->A) or `ascending' (sorting A->Z).")
+
+(defcustom mu4e-headers-from-or-to-prefix '("" . "To ")
+ "Prefix for the :from-or-to field when it is showing,
+ respectively, From: or To:. It is a cons cell with the car
+ element being the From: prefix, the cdr element the To: prefix."
+ :type '(cons string string)
+ :group 'mu4e-headers)
+
+;;;; Fancy marks
+
+;; marks for headers of the form; each is a cons-cell (basic . fancy)
+;; each of which is basic ascii char and something fancy, respectively
+;; by default, we some conservative marks, even when 'fancy'
+;; so they're less likely to break if people don't have certain fonts.
+;; However, if you want to be really 'fancy', you could use something like
+;; the following; esp. with a newer Emacs with color-icon support.
+;; (setq
+;; mu4e-headers-draft-mark '("D" . "💈")
+;; mu4e-headers-flagged-mark '("F" . "📍")
+;; mu4e-headers-new-mark '("N" . "🔥")
+;; mu4e-headers-passed-mark '("P" . "❯")
+;; mu4e-headers-replied-mark '("R" . "❮")
+;; mu4e-headers-seen-mark '("S" . "☑")
+;; mu4e-headers-trashed-mark '("T" . "💀")
+;; mu4e-headers-attach-mark '("a" . "📎")
+;; mu4e-headers-encrypted-mark '("x" . "🔒")
+;; mu4e-headers-signed-mark '("s" . "🔑")
+;; mu4e-headers-unread-mark '("u" . "⎕")
+;; mu4e-headers-list-mark '("s" . "🔈")
+;; mu4e-headers-personal-mark '("p" . "👨")
+;; mu4e-headers-calendar-mark '("c" . "📅"))
+
+
+(defvar mu4e-headers-draft-mark '("D" . "⚒") "Draft.")
+(defvar mu4e-headers-flagged-mark '("F" . "✚") "Flagged.")
+(defvar mu4e-headers-new-mark '("N" . "✱") "New.")
+(defvar mu4e-headers-passed-mark '("P" . "❯") "Passed (fwd).")
+(defvar mu4e-headers-replied-mark '("R" . "❮") "Replied.")
+(defvar mu4e-headers-seen-mark '("S" . "✔") "Seen.")
+(defvar mu4e-headers-trashed-mark '("T" . "⏚") "Trashed.")
+(defvar mu4e-headers-attach-mark '("a" . "⚓") "W/ attachments.")
+(defvar mu4e-headers-encrypted-mark '("x" . "⚴") "Encrypted.")
+(defvar mu4e-headers-signed-mark '("s" . "☡") "Signed.")
+(defvar mu4e-headers-unread-mark '("u" . "⎕") "Unread.")
+(defvar mu4e-headers-list-mark '("s" . "Ⓛ") "Mailing list.")
+(defvar mu4e-headers-personal-mark '("p" . "Ⓟ") "Personal.")
+(defvar mu4e-headers-calendar-mark '("c" . "Ⓒ") "Calendar invitation.")
+
+
+;;;; Graph drawing
+
+(defvar mu4e-headers-thread-mark-as-orphan 'first
+ "Define which messages should be prefixed with the orphan mark.
+`all' marks all the messages without a parent as orphan, `first'
+only marks the first message in the thread.")
+
+(defvar mu4e-headers-thread-root-prefix '("* " . "□ ")
+ "Prefix for root messages.")
+(defvar mu4e-headers-thread-child-prefix '("|>" . "│ ")
+ "Prefix for messages in sub threads that do have a following sibling.")
+(defvar mu4e-headers-thread-first-child-prefix '("o " . "⚬ ")
+ "Prefix for the first child messages in a sub thread.")
+(defvar mu4e-headers-thread-last-child-prefix '("L" . "└ ")
+ "Prefix for messages in sub threads that do not have a following sibling.")
+(defvar mu4e-headers-thread-connection-prefix '("|" . "│ ")
+ "Prefix to connect sibling messages that do not follow each other.
+Must have the same length as `mu4e-headers-thread-blank-prefix'.")
+(defvar mu4e-headers-thread-blank-prefix '(" " . " ")
+ "Prefix to separate non connected messages.
+Must have the same length as `mu4e-headers-thread-connection-prefix'.")
+(defvar mu4e-headers-thread-orphan-prefix '("<>" . "♢ ")
+ "Prefix for orphan messages with siblings.")
+(defvar mu4e-headers-thread-single-orphan-prefix '("<>" . "♢ ")
+ "Prefix for orphan messages with no siblings.")
+(defvar mu4e-headers-thread-duplicate-prefix '("=" . "≡ ")
+ "Prefix for duplicate messages.")
+
+(defvar mu4e-headers-threaded-label '("T" . "Ⓣ")
+ "Non-fancy and fancy labels to indicate threaded search in the mode-line.")
+(defvar mu4e-headers-full-label '("F" . "Ⓕ")
+ "Non-fancy and fancy labels to indicate full search in the mode-line.")
+(defvar mu4e-headers-related-label '("R" . "Ⓡ")
+ "Non-fancy and fancy labels to indicate related search in the mode-line.")
+(defvar mu4e-headers-skip-duplicates-label '("U" . "Ⓤ") ;; 'U' for 'unique'
+ "Non-fancy and fancy labels for include-related search in the mode-line.")
+
+;;;; Various
+
+(defvar mu4e-headers-actions
+ '( ("capture message" . mu4e-action-capture-message)
+ ("show this thread" . mu4e-action-show-thread))
+ "List of actions to perform on messages in the headers list.
+The actions are cons-cells of the form (NAME . FUNC) where:
+* NAME is the name of the action (e.g. \"Count lines\")
+* FUNC is a function which receives a message plist as an argument.
+
+The first character of NAME is used as the shortcut.")
+
+(defvar mu4e-headers-custom-markers
+ '(("Older than"
+ (lambda (msg date) (time-less-p (mu4e-msg-field msg :date) date))
+ (lambda () (mu4e-get-time-date "Match messages before: ")))
+ ("Newer than"
+ (lambda (msg date) (time-less-p date (mu4e-msg-field msg :date)))
+ (lambda () (mu4e-get-time-date "Match messages after: ")))
+ ("Bigger than"
+ (lambda (msg bytes) (> (mu4e-msg-field msg :size) (* 1024 bytes)))
+ (lambda () (read-number "Match messages bigger than (Kbytes): "))))
+ "List of custom markers -- functions to mark message that match
+some custom function. Each of the list members has the following format:
+ (NAME PREDICATE-FUNC PARAM-FUNC)
+* NAME is the name of the predicate function, and the first character
+is the shortcut (so keep those unique).
+* PREDICATE-FUNC is a function that takes two parameters, MSG
+and (optionally) PARAM, and should return non-nil when there's a
+match.
+* PARAM-FUNC is function that is evaluated once, and its value is then passed to
+PREDICATE-FUNC as PARAM. This is useful for getting user-input.")
+;;; Internal variables/constants
+
+;; docid cookies
+(defconst mu4e~headers-docid-pre "\376"
+ "Each header starts (invisibly) with the `mu4e~headers-docid-pre',
+followed by the docid, followed by `mu4e~headers-docid-post'.")
+(defconst mu4e~headers-docid-post "\377"
+ "Each header starts (invisibly) with the `mu4e~headers-docid-pre',
+followed by the docid, followed by `mu4e~headers-docid-post'.")
+
+(defvar mu4e~headers-sort-field-choices
+ '( ("date" . :date)
+ ("from" . :from)
+ ("list" . :list)
+ ("maildir" . :maildir)
+ ("prio" . :prio)
+ ("zsize" . :size)
+ ("subject" . :subject)
+ ("to" . :to))
+ "List of cells describing the various sort-options.
+In the format needed for `mu4e-read-option'.")
+
+
+(defvar mu4e~headers-search-start nil)
+(defvar mu4e~headers-render-start nil)
+(defvar mu4e~headers-render-time nil)
+
+(defvar mu4e-headers-report-render-time nil
+ "If non-nil, report on the time it took to render the messages.
+This is mostly useful for profiling.")
+
+\f
+
+;;; Clear
+
+(defun mu4e~headers-clear (&optional text)
+ "Clear the headers buffer and related data structures.
+Optionally, show TEXT."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (setq mu4e~headers-render-start (float-time))
+ (let ((inhibit-read-only t))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (mu4e--mark-clear)
+ (erase-buffer)
+ (when text
+ (goto-char (point-min))
+ (insert (propertize text 'face 'mu4e-system-face 'intangible t)))))))
+
+\f
+;;; Misc
+
+(defun mu4e~headers-contact-str (contacts)
+ "Turn the list of contacts CONTACTS (with elements (NAME . EMAIL)
+into a string."
+ (mapconcat
+ (lambda (contact)
+ (let ((name (mu4e-contact-name contact))
+ (email (mu4e-contact-email contact)))
+ (or name email "?"))) contacts ", "))
+
+(defun mu4e~headers-thread-prefix-map (type)
+ "Return the thread prefix based on the symbol TYPE."
+ (let ((get-prefix
+ (lambda (cell)
+ (if mu4e-use-fancy-chars (cdr cell) (car cell)))))
+ (cl-case type
+ (child (funcall get-prefix mu4e-headers-thread-child-prefix))
+ (first-child (funcall get-prefix mu4e-headers-thread-first-child-prefix))
+ (last-child (funcall get-prefix mu4e-headers-thread-last-child-prefix))
+ (connection (funcall get-prefix mu4e-headers-thread-connection-prefix))
+ (blank (funcall get-prefix mu4e-headers-thread-blank-prefix))
+ (orphan (funcall get-prefix mu4e-headers-thread-orphan-prefix))
+ (single-orphan (funcall get-prefix mu4e-headers-thread-single-orphan-prefix))
+ (duplicate (funcall get-prefix mu4e-headers-thread-duplicate-prefix))
+ (t "?"))))
+
+
+;; headers in the buffer are prefixed by an invisible string with the docid
+;; followed by an EOT ('end-of-transmission', \004, ^D) non-printable ascii
+;; character. this string also has a text-property with the docid. the former
+;; is used for quickly finding a certain header, the latter for retrieving the
+;; docid at point without string matching etc.
+
+(defun mu4e~headers-docid-pos (docid)
+ "Return the pos of the beginning of the line with the header with
+docid DOCID, or nil if it cannot be found."
+ (let ((pos))
+ (save-excursion
+ (setq pos (mu4e~headers-goto-docid docid)))
+ pos))
+
+(defun mu4e~headers-docid-cookie (docid)
+ "Create an invisible string containing DOCID; this is to be used
+at the beginning of lines to identify headers."
+ (propertize (format "%s%d%s"
+ mu4e~headers-docid-pre docid mu4e~headers-docid-post)
+ 'docid docid 'invisible t));;
+
+(defun mu4e~headers-docid-at-point (&optional point)
+ "Get the docid for the header at POINT, or at current (point) if
+nil. Returns the docid, or nil if there is none."
+ (save-excursion
+ (when point
+ (goto-char point))
+ (get-text-property (line-beginning-position) 'docid)))
+
+
+
+(defun mu4e~headers-goto-docid (docid &optional to-mark)
+ "Go to the beginning of the line with the header with docid
+DOCID, or nil if it cannot be found. If the optional TO-MARK is
+non-nil, go to the point directly *after* the docid-cookie instead
+of the beginning of the line."
+ (let ((oldpoint (point)) (newpoint))
+ (goto-char (point-min))
+ (setq newpoint
+ (search-forward (mu4e~headers-docid-cookie docid) nil t))
+ (unless to-mark
+ (if (null newpoint)
+ (goto-char oldpoint) ;; not found; restore old pos
+ (progn
+ (beginning-of-line) ;; found, move to beginning of line
+ (setq newpoint (point)))))
+ newpoint)) ;; return the point, or nil if not found
+
+(defun mu4e~headers-field-for-docid (docid field)
+ "Get FIELD (a symbol, see `mu4e-headers-names') for the message
+with DOCID which must be present in the headers buffer."
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-message-field (mu4e-message-at-point) field))))
+
+
+;; In order to print a thread tree with all the message connections,
+;; it's necessary to keep track of all sub levels that still have
+;; following messages. For each level, mu4e~headers-thread-state keeps
+;; the value t for a connection or nil otherwise.
+(defvar-local mu4e~headers-thread-state '())
+
+(defun mu4e~headers-thread-prefix (thread)
+ "Calculate the thread prefix based on thread info THREAD."
+ (when thread
+ (let* ((prefix "")
+ (level (plist-get thread :level))
+ (has-child (plist-get thread :has-child))
+ (first-child (plist-get thread :first-child))
+ (last-child (plist-get thread :last-child))
+ (orphan (plist-get thread :orphan))
+ (single-orphan(and orphan first-child last-child))
+ (duplicate (plist-get thread :duplicate)))
+ ;; Do not prefix root messages.
+ (if (= level 0)
+ (setq mu4e~headers-thread-state '()))
+ (if (> level 0)
+ (let* ((length (length mu4e~headers-thread-state))
+ (padding (make-list (max 0 (- level length)) nil)))
+ ;; Trim and pad the state to ensure a message will
+ ;; always be shown with the correct indentation, even if
+ ;; a broken thread is returned. It's trimmed to level-1
+ ;; because the current level has always an connection
+ ;; and it used a special formatting.
+ (setq mu4e~headers-thread-state
+ (cl-subseq (append mu4e~headers-thread-state padding)
+ 0 (- level 1)))
+ ;; Prepare the thread prefix.
+ (setq prefix
+ (concat
+ ;; Current mu4e~headers-thread-state, composed by
+ ;; connections or blanks.
+ (mapconcat
+ (lambda (s)
+ (mu4e~headers-thread-prefix-map
+ (if s 'connection 'blank)))
+ mu4e~headers-thread-state "")
+ ;; Current entry.
+ (mu4e~headers-thread-prefix-map
+ (if single-orphan 'single-orphan
+ (if (and orphan
+ (or first-child
+ (not (eq mu4e-headers-thread-mark-as-orphan 'first))))
+ 'orphan
+ (if last-child 'last-child
+ (if first-child 'first-child
+ 'child)))))))))
+ ;; If a new sub-thread will follow (has-child) and the current
+ ;; one is still not done (not last-child), then a new
+ ;; connection needs to be added to the tree-state. It's not
+ ;; necessary to a blank (nil), because padding will handle
+ ;; that.
+ (if (and has-child (not last-child))
+ (setq mu4e~headers-thread-state
+ (append mu4e~headers-thread-state '(t))))
+ ;; Return the thread prefix.
+ (format "%s%s"
+ prefix
+ (if duplicate
+ (mu4e~headers-thread-prefix-map 'duplicate) "")))))
+
+(defun mu4e~headers-flags-str (flags)
+ "Get a display string for FLAGS.
+Note that `mu4e-flags-to-string' is for internal use only; this
+function is for display. (This difference is significant, since
+internally, the Maildir spec determines what the flags look like,
+while our display may be different)."
+ (or (mapconcat
+ (lambda (flag)
+ (when (member flag mu4e-headers-visible-flags)
+ (if-let* ((mark (intern-soft
+ (format "mu4e-headers-%s-mark" (symbol-name flag))))
+ (cell (symbol-value mark)))
+ (if mu4e-use-fancy-chars (cdr cell) (car cell))
+ "")))
+ flags "")
+ ""))
+
+;;; Special headers
+
+(defun mu4e~headers-from-or-to (msg)
+ "Get the From: address from MSG if not one of user's; otherwise get To:.
+When the from address for message MSG is one of the the user's
+addresses, (as per `mu4e-personal-address-p'), show the To
+address. Otherwise, show the From address, prefixed with the
+appropriate `mu4e-headers-from-or-to-prefix'."
+ (let* ((from1 (car-safe (mu4e-message-field msg :from)))
+ (from1-addr (and from1 (mu4e-contact-email from1)))
+ (is-user (and from1-addr (mu4e-personal-address-p from1-addr))))
+ (if is-user
+ (concat (cdr mu4e-headers-from-or-to-prefix)
+ (mu4e~headers-contact-str (mu4e-message-field msg :to)))
+ (concat (car mu4e-headers-from-or-to-prefix)
+ (mu4e~headers-contact-str (mu4e-message-field msg :from))))))
+
+(defun mu4e~headers-human-date (msg)
+ "Show a \"human\" date for MSG.
+If the date is today, show the time, otherwise, show the date.
+The formats used for date and time are `mu4e-headers-date-format'
+and `mu4e-headers-time-format'."
+ (let ((date (mu4e-msg-field msg :date)))
+ (if (equal date '(0 0 0))
+ "None"
+ (let ((day1 (decode-time date))
+ (day2 (decode-time (current-time))))
+ (if (and
+ (eq (nth 3 day1) (nth 3 day2)) ;; day
+ (eq (nth 4 day1) (nth 4 day2)) ;; month
+ (eq (nth 5 day1) (nth 5 day2))) ;; year
+ (format-time-string mu4e-headers-time-format date)
+ (format-time-string mu4e-headers-date-format date))))))
+
+(defun mu4e~headers-thread-subject (msg)
+ "Get the subject for MSG if it is the first one in a thread.
+Otherwise, return the thread-prefix without the subject-text. In
+other words, show the subject of a thread only once, similar to
+e.g. \"mutt\"."
+ (let* ((tinfo (mu4e-message-field msg :meta))
+ (subj (mu4e-msg-field msg :subject)))
+ (concat ;; prefix subject with a thread indicator
+ (mu4e~headers-thread-prefix tinfo)
+ (if (plist-get tinfo :thread-subject)
+ (truncate-string-to-width subj 600) ""))))
+
+(defun mu4e~headers-mailing-list (list)
+ "Get some identifier for the mailing list."
+ (if list
+ (propertize (mu4e-get-mailing-list-shortname list) 'help-echo list)
+ ""))
+
+(defsubst mu4e~headers-custom-field-value (msg field)
+ "Show some custom header field, or raise an error if it is not
+found."
+ (let* ((item (or (assoc field mu4e-header-info-custom)
+ (mu4e-error "field %S not found" field)))
+ (func (or (plist-get (cdr-safe item) :function)
+ (mu4e-error "no :function defined for field %S %S"
+ field (cdr item)))))
+ (funcall func msg)))
+
+(defun mu4e~headers-field-value (msg field)
+ (let ((val (mu4e-message-field msg field)))
+ (cl-case field
+ (:subject
+ (concat ;; prefix subject with a thread indicator
+ (mu4e~headers-thread-prefix (mu4e-message-field msg :meta))
+ ;; "["(plist-get (mu4e-message-field msg :meta) :path) "] "
+ ;; work-around: emacs' display gets really slow when lines are too long;
+ ;; so limit subject length to 600
+ (truncate-string-to-width val 600)))
+ (:thread-subject ;; if not searching threads, fall back to :subject
+ (if mu4e-search-threads
+ (mu4e~headers-thread-subject msg)
+ (mu4e~headers-field-value msg :subject)))
+ ((:maildir :path :message-id) val)
+ ((:to :from :cc :bcc) (mu4e~headers-contact-str val))
+ ;; if we (ie. `user-mail-address' is the 'From', show
+ ;; 'To', otherwise show From
+ (:from-or-to (mu4e~headers-from-or-to msg))
+ (:date (format-time-string mu4e-headers-date-format val))
+ (:list (or val ""))
+ (:mailing-list (mu4e~headers-mailing-list (mu4e-msg-field msg :list)))
+ (:human-date (propertize (mu4e~headers-human-date msg)
+ 'help-echo (format-time-string
+ mu4e-headers-long-date-format
+ (mu4e-msg-field msg :date))))
+ (:flags (propertize (mu4e~headers-flags-str val)
+ 'help-echo (format "%S" val)))
+ (:tags (propertize (mapconcat 'identity val ", ")))
+ (:size (mu4e-display-size val))
+ (t (mu4e~headers-custom-field-value msg field)))))
+
+(defsubst mu4e~headers-truncate-field-fast (val width)
+ "Truncate VAL to WIDTH. Fast and somewhat inaccurate."
+ (if width
+ (truncate-string-to-width val width 0 ?\s truncate-string-ellipsis)
+ val))
+
+(defun mu4e~headers-truncate-field-precise (field val width)
+ "Return VAL truncated to one less than WIDTH, with a trailing
+space propertized with a `display' text property which expands to
+ the correct column for display."
+ (when width
+ (let ((end-col (cl-loop for (f . w) in mu4e-headers-fields
+ sum w
+ until (equal f field))))
+ (setq val (string-trim-right val))
+ (if (> width (length val))
+ (setq val (concat val " "))
+ (setq val
+ (concat
+ (truncate-string-to-width val (1- width) 0 ?\s t)
+ " ")))
+ (put-text-property (1- (length val))
+ (length val)
+ 'display
+ `(space . (:align-to ,end-col))
+ val)))
+ val)
+
+(defsubst mu4e~headers-truncate-field (field val width)
+ "Truncate VAL to WIDTH."
+ (if mu4e-headers-precise-alignment
+ (mu4e~headers-truncate-field-precise field val width)
+ (mu4e~headers-truncate-field-fast val width)))
+
+(make-obsolete-variable 'mu4e-headers-field-properties-function
+ "not used" "1.6.1")
+
+(defsubst mu4e~headers-field-handler (f-w msg)
+ "Create a description of the field of MSG described by F-W."
+ (let* ((field (car f-w))
+ (width (cdr f-w))
+ (val (mu4e~headers-field-value msg field))
+ (val (and val (if width (mu4e~headers-truncate-field field val width) val))))
+ val))
+
+(defsubst mu4e~headers-apply-flags (msg fieldval)
+ "Adjust FIELDVAL's face property based on flags in MSG."
+ (let* ((flags (plist-get msg :flags))
+ (meta (plist-get msg :meta))
+ (face (cond
+ ((memq 'trashed flags) 'mu4e-trashed-face)
+ ((memq 'draft flags) 'mu4e-draft-face)
+ ((or (memq 'unread flags) (memq 'new flags))
+ 'mu4e-unread-face)
+ ((memq 'flagged flags) 'mu4e-flagged-face)
+ ((plist-get meta :related) 'mu4e-related-face)
+ ((memq 'replied flags) 'mu4e-replied-face)
+ ((memq 'passed flags) 'mu4e-forwarded-face)
+ (t 'mu4e-header-face))))
+ (add-face-text-property 0 (length fieldval) face t fieldval)
+ fieldval))
+
+(defsubst mu4e~message-header-line (msg)
+ "Return a propertized description of MSG suitable for
+displaying in the header view."
+ (unless (and mu4e-headers-hide-predicate
+ (funcall mu4e-headers-hide-predicate msg))
+ (mu4e~headers-apply-flags
+ msg
+ (mapconcat (lambda (f-w) (mu4e~headers-field-handler f-w msg))
+ mu4e-headers-fields " "))))
+
+
+(defsubst mu4e~headers-insert-header (msg pos)
+ "Insert a header for MSG at point POS."
+ (when-let ((line (mu4e~message-header-line msg))
+ (docid (plist-get msg :docid)))
+ (goto-char pos)
+ (insert
+ (propertize
+ (concat
+ (mu4e~headers-docid-cookie docid)
+ mu4e--mark-fringe line "\n")
+ 'docid docid 'msg msg))))
+
+(defun mu4e~headers-remove-header (docid &optional ignore-missing)
+ "Remove header with DOCID at point.
+When IGNORE-MISSING is non-nill, don't raise an error when the
+docid is not found."
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (if (mu4e~headers-goto-docid docid)
+ (let ((inhibit-read-only t))
+ (delete-region (line-beginning-position) (line-beginning-position 2)))
+ (unless ignore-missing
+ (mu4e-error "Cannot find message with docid %S" docid)))))
+
+\f
+;;; Handler functions
+
+;; next are a bunch of handler functions; those will be called from mu4e~proc in
+;; response to output from the server process
+
+(defun mu4e~headers-view-handler (msg)
+ "Handler function for displaying a message."
+ (mu4e-view msg))
+
+(defun mu4e~headers-view-this-message-p (docid)
+ "Is DOCID currently being viewed?"
+ (when (buffer-live-p (mu4e-get-view-buffer))
+ (with-current-buffer (mu4e-get-view-buffer)
+ (eq docid (plist-get mu4e~view-message :docid)))))
+
+;; note: this function is very performance-sensitive
+(defun mu4e~headers-append-handler (msglst)
+ "Append one-line descriptions of messages in MSGLIST.
+Do this at the end of the headers-buffer."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (save-excursion
+ (let ((inhibit-read-only t))
+ (seq-do
+ (lambda (msg)
+ (mu4e~headers-insert-header msg (point-max)))
+ msglst))))))
+
+
+(defun mu4e~headers-update-handler (msg is-move maybe-view)
+ "Update handler, will be called when a message has been updated
+in the database. This function will update the current list of
+headers."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (let* ((docid (mu4e-message-field msg :docid))
+ (initial-message-at-point (mu4e~headers-docid-at-point))
+ (initial-column (current-column))
+ (inhibit-read-only t)
+ (point (mu4e~headers-docid-pos docid))
+ (markinfo (gethash docid mu4e--mark-map)))
+ (when point ;; is the message present in this list?
+
+ ;; if it's marked, unmark it now
+ (when (mu4e-mark-docid-marked-p docid)
+ (mu4e-mark-set 'unmark))
+
+ ;; re-use the thread info from the old one; this is needed because
+ ;; *update* messages don't have thread info by themselves (unlike
+ ;; search results)
+ ;; since we still have the search results, re-use
+ ;; those
+ (plist-put msg :meta
+ (mu4e~headers-field-for-docid docid :meta))
+
+ ;; first, remove the old one (otherwise, we'd have two headers with
+ ;; the same docid...
+ (mu4e~headers-remove-header docid t)
+
+ ;; if we're actually viewing this message (in mu4e-view mode), we
+ ;; update it; that way, the flags can be updated, as well as the path
+ ;; (which is useful for viewing the raw message)
+ (when (and maybe-view (mu4e~headers-view-this-message-p docid))
+ (save-excursion (mu4e-view msg)))
+ ;; now, if this update was about *moving* a message, we don't show it
+ ;; anymore (of course, we cannot be sure if the message really no
+ ;; longer matches the query, but this seem a good heuristic. if it
+ ;; was only a flag-change, show the message with its updated flags.
+ (unless is-move
+ (save-excursion
+ (mu4e~headers-insert-header msg point)))
+
+ ;; restore the mark, if any. See #2076.
+ (when (and markinfo (mu4e~headers-goto-docid docid))
+ (mu4e-mark-at-point (car markinfo) (cdr markinfo)))
+
+ (if (and initial-message-at-point
+ (mu4e~headers-goto-docid initial-message-at-point))
+ (progn
+ (move-to-column initial-column)
+ (mu4e~headers-highlight initial-message-at-point))
+ ;; attempt to highlight the corresponding line and make it visible
+ (mu4e~headers-highlight docid))
+ (run-hooks 'mu4e-message-changed-hook))))))
+
+(defun mu4e~headers-remove-handler (docid)
+ "Remove handler, will be called when a message with DOCID has
+been removed from the database. This function will hide the removed
+message from the current list of headers. If the message is not
+present, don't do anything."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (mu4e~headers-remove-header docid t))
+ ;; if we were viewing this message, close it now.
+ (when (and (mu4e~headers-view-this-message-p docid)
+ (buffer-live-p (mu4e-get-view-buffer)))
+ (unless (eq mu4e-split-view 'single-window)
+ (mapc #'delete-window (get-buffer-window-list
+ (mu4e-get-view-buffer) nil t)))
+ (kill-buffer (mu4e-get-view-buffer))))
+
+
+\f
+;;; Performing queries (internal)
+(defconst mu4e~search-message "Searching...")
+(defconst mu4e~no-matches "No matching messages found")
+(defconst mu4e~end-of-results "End of search results")
+
+(defun mu4e--search-execute (expr ignore-history)
+ "Search for query EXPR.
+
+Switch to the output buffer for the results. If IGNORE-HISTORY is
+true, do *not* update the query history stack."
+ (let* ((buf (get-buffer-create mu4e-headers-buffer-name))
+ (inhibit-read-only t)
+ (rewritten-expr (funcall mu4e-query-rewrite-function expr))
+ (maxnum (unless mu4e-search-full mu4e-search-results-limit)))
+ (with-current-buffer buf
+ (mu4e-headers-mode)
+ (unless ignore-history
+ ;; save the old present query to the history list
+ (when mu4e--search-last-query
+ (mu4e--search-push-query mu4e--search-last-query 'past)))
+ (setq mu4e--search-last-query rewritten-expr)
+ (setq list-buffers-directory rewritten-expr)
+ (mu4e~headers-update-mode-line))
+
+ ;; when the buffer is already visible, select it; otherwise,
+ ;; switch to it.
+ (unless (get-buffer-window buf 0)
+ (switch-to-buffer buf))
+ (run-hook-with-args 'mu4e-search-hook expr)
+ (mu4e~headers-clear mu4e~search-message)
+ (setq mu4e~headers-search-start (float-time))
+ (mu4e--server-find
+ rewritten-expr
+ mu4e-search-threads
+ mu4e-headers-sort-field
+ mu4e-headers-sort-direction
+ maxnum
+ mu4e-headers-skip-duplicates
+ mu4e-headers-include-related)))
+
+(defun mu4e~headers-benchmark-message (count)
+ "Get some report message for messaging search and rendering speed."
+ (if (and mu4e-headers-report-render-time
+ mu4e~headers-search-start
+ mu4e~headers-render-start
+ (> count 0))
+ (let ((render-time-ms (* 1000(- (float-time) mu4e~headers-render-start)))
+ (search-time-ms (* 1000(- (float-time) mu4e~headers-search-start))))
+ (format (concat
+ "; search: %0.1f ms (%0.2f ms/msg)"
+ "; render: %0.1f ms (%0.2f ms/msg)")
+ search-time-ms (/ search-time-ms count)
+ render-time-ms (/ render-time-ms count)))
+ ""))
+
+(defun mu4e~headers-found-handler (count)
+ "Create a one line description of the number of headers found
+after the end of the search results."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (save-excursion
+ (goto-char (point-max))
+ (let ((inhibit-read-only t)
+ (str (if (zerop count) mu4e~no-matches mu4e~end-of-results))
+ (msg (format "Found %d matching message%s%s"
+ count (if (= 1 count) "" "s")
+ (mu4e~headers-benchmark-message count))))
+
+ (insert (propertize str 'face 'mu4e-system-face 'intangible t))
+ (unless (zerop count)
+ (mu4e-message "%s" msg))))
+
+ ;; if we need to jump to some specific message, do so now
+ (goto-char (point-min))
+ (when mu4e--search-msgid-target
+ (if (eq (current-buffer) (window-buffer))
+ (mu4e-headers-goto-message-id mu4e--search-msgid-target)
+ (let* ((pos (mu4e-headers-goto-message-id mu4e--search-msgid-target)))
+ (when pos
+ (set-window-point (get-buffer-window nil t) pos)))))
+ (when (and mu4e--search-view-target (mu4e-message-at-point 'noerror))
+ ;; view the message at point when there is one.
+ (mu4e-headers-view-message))
+ (setq mu4e--search-view-target nil
+ mu4e--search-msgid-target nil)
+ (when (mu4e~headers-docid-at-point)
+ (mu4e~headers-highlight (mu4e~headers-docid-at-point)))))
+ ;; run-hooks
+ (run-hooks 'mu4e-headers-found-hook))
+
+\f
+;;; Marking
+
+(defmacro mu4e~headers-defun-mark-for (mark)
+ "Define a function mu4e~headers-mark-MARK."
+ (let ((funcname (intern (format "mu4e-headers-mark-for-%s" mark)))
+ (docstring (format "Mark header at point with %s." mark)))
+ `(progn
+ (defun ,funcname () ,docstring
+ (interactive)
+ (mu4e-headers-mark-and-next ',mark))
+ (put ',funcname 'definition-name ',mark))))
+
+(mu4e~headers-defun-mark-for refile)
+(mu4e~headers-defun-mark-for something)
+(mu4e~headers-defun-mark-for delete)
+(mu4e~headers-defun-mark-for trash)
+(mu4e~headers-defun-mark-for flag)
+(mu4e~headers-defun-mark-for move)
+(mu4e~headers-defun-mark-for read)
+(mu4e~headers-defun-mark-for unflag)
+(mu4e~headers-defun-mark-for untrash)
+(mu4e~headers-defun-mark-for unmark)
+(mu4e~headers-defun-mark-for unread)
+(mu4e~headers-defun-mark-for action)
+
+;;; Headers-mode and mode-map
+
+(defvar mu4e-headers-mode-map nil
+ "Keymap for *mu4e-headers* buffers.")
+(unless mu4e-headers-mode-map
+ (setq mu4e-headers-mode-map
+ (let ((map (make-sparse-keymap)))
+
+ (define-key map "j" 'mu4e~headers-jump-to-maildir)
+ (define-key map "O" 'mu4e-headers-change-sorting)
+ (define-key map "M" 'mu4e-headers-toggle-setting)
+
+ ;; these are impossible to remember; use mu4e-headers-toggle-setting
+ ;; instead :)
+ (define-key map "P" 'mu4e-headers-toggle-threading)
+ (define-key map "Q" 'mu4e-headers-toggle-full-search)
+ (define-key map "W" 'mu4e-headers-toggle-include-related)
+ (define-key map "V" 'mu4e-headers-toggle-skip-duplicates)
+
+ (define-key map "q" 'mu4e~headers-quit-buffer)
+ (define-key map "g" 'mu4e-search-rerun) ;; for compatibility
+
+ (define-key map "%" 'mu4e-headers-mark-pattern)
+ (define-key map "t" 'mu4e-headers-mark-subthread)
+ (define-key map "T" 'mu4e-headers-mark-thread)
+
+ (define-key map "," #'mu4e-sexp-at-point)
+
+ ;; navigation between messages
+ (define-key map "p" 'mu4e-headers-prev)
+ (define-key map "n" 'mu4e-headers-next)
+ (define-key map (kbd "<M-up>") 'mu4e-headers-prev)
+ (define-key map (kbd "<M-down>") 'mu4e-headers-next)
+
+ (define-key map (kbd "[") 'mu4e-headers-prev-unread)
+ (define-key map (kbd "]") 'mu4e-headers-next-unread)
+
+ ;; change the number of headers
+ (define-key map (kbd "C-+") 'mu4e-headers-split-view-grow)
+ (define-key map (kbd "C--") 'mu4e-headers-split-view-shrink)
+ (define-key map (kbd "<C-kp-add>") 'mu4e-headers-split-view-grow)
+ (define-key map (kbd "<C-kp-subtract>") 'mu4e-headers-split-view-shrink)
+
+ ;; switching to view mode (if it's visible)
+ (define-key map "y" 'mu4e-select-other-view)
+
+ ;; marking/unmarking ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+ (define-key map (kbd "<backspace>") 'mu4e-headers-mark-for-trash)
+ (define-key map (kbd "d") 'mu4e-headers-mark-for-trash)
+ (define-key map (kbd "<delete>") 'mu4e-headers-mark-for-delete)
+ (define-key map (kbd "<deletechar>") 'mu4e-headers-mark-for-delete)
+ (define-key map (kbd "D") 'mu4e-headers-mark-for-delete)
+ (define-key map (kbd "m") 'mu4e-headers-mark-for-move)
+ (define-key map (kbd "r") 'mu4e-headers-mark-for-refile)
+
+ (define-key map (kbd "?") 'mu4e-headers-mark-for-unread)
+ (define-key map (kbd "!") 'mu4e-headers-mark-for-read)
+ (define-key map (kbd "A") 'mu4e-headers-mark-for-action)
+
+ (define-key map (kbd "u") 'mu4e-headers-mark-for-unmark)
+ (define-key map (kbd "+") 'mu4e-headers-mark-for-flag)
+ (define-key map (kbd "-") 'mu4e-headers-mark-for-unflag)
+ (define-key map (kbd "=") 'mu4e-headers-mark-for-untrash)
+ (define-key map (kbd "&") 'mu4e-headers-mark-custom)
+
+ (define-key map (kbd "*") 'mu4e-headers-mark-for-something)
+ (define-key map (kbd "<kp-multiply>") 'mu4e-headers-mark-for-something)
+ (define-key map (kbd "<insertchar>") 'mu4e-headers-mark-for-something)
+ (define-key map (kbd "<insert>") 'mu4e-headers-mark-for-something)
+
+ (define-key map (kbd "#") 'mu4e-mark-resolve-deferred-marks)
+
+ (define-key map "U" 'mu4e-mark-unmark-all)
+ (define-key map "x" 'mu4e-mark-execute-all)
+
+ (define-key map "a" 'mu4e-headers-action)
+
+ ;; message composition
+ (define-key map "R" 'mu4e-compose-reply)
+ (define-key map "F" 'mu4e-compose-forward)
+ (define-key map "C" 'mu4e-compose-new)
+ (define-key map "E" 'mu4e-compose-edit)
+
+ (define-key map (kbd "RET") 'mu4e-headers-view-message)
+ (define-key map [mouse-2] 'mu4e-headers-view-message)
+
+ (define-key map "$" 'mu4e-show-log)
+ (define-key map "H" 'mu4e-display-manual)
+
+ (define-key map "|" 'mu4e-view-pipe)
+
+ ;; menu
+ ;;(define-key map [menu-bar] (make-sparse-keymap))
+ (let ((menumap (make-sparse-keymap)))
+ (define-key map [menu-bar headers] (cons "Mu4e" menumap))
+
+ (define-key menumap [mu4e~headers-quit-buffer]
+ '("Quit view" . mu4e~headers-quit-buffer))
+ (define-key menumap [display-help] '("Help" . mu4e-display-manual))
+
+ (define-key menumap [sepa0] '("--"))
+
+ (define-key menumap [toggle-include-related]
+ '(menu-item "Toggle related messages"
+ mu4e-headers-toggle-include-related
+ :button (:toggle .
+ (and (boundp 'mu4e-headers-include-related)
+ mu4e-headers-include-related))))
+ (define-key menumap [toggle-threading]
+ '(menu-item "Toggle threading" mu4e-headers-toggle-threading
+ :button (:toggle .
+ (and (boundp 'mu4e-search-threads)
+ mu4e-search-threads))))
+
+ (define-key menumap "|" '("Pipe through shell" . mu4e-view-pipe))
+ (define-key menumap [sepa1] '("--"))
+
+ (define-key menumap [execute-marks] '("Execute marks"
+ . mu4e-mark-execute-all))
+ (define-key menumap [unmark-all] '("Unmark all" . mu4e-mark-unmark-all))
+ (define-key menumap [unmark]
+ '("Unmark" . mu4e-headers-mark-for-unmark))
+
+ (define-key menumap [mark-pattern] '("Mark pattern" .
+ mu4e-headers-mark-pattern))
+ (define-key menumap [mark-as-read] '("Mark as read" .
+ mu4e-headers-mark-for-read))
+ (define-key menumap [mark-as-unread]
+ '("Mark as unread" . mu4e-headers-mark-for-unread))
+
+ (define-key menumap [mark-delete]
+ '("Mark for deletion" . mu4e-headers-mark-for-delete))
+ (define-key menumap [mark-untrash]
+ '("Mark for untrash" . mu4e-headers-mark-for-untrash))
+ (define-key menumap [mark-trash]
+ '("Mark for trash" . mu4e-headers-mark-for-trash))
+ (define-key menumap [mark-move]
+ '("Mark for move" . mu4e-headers-mark-for-move))
+ (define-key menumap [sepa2] '("--"))
+
+ (define-key menumap [resend] '("Resend" . mu4e-compose-resend))
+ (define-key menumap [forward] '("Forward" . mu4e-compose-forward))
+ (define-key menumap [reply] '("Reply" . mu4e-compose-reply))
+ (define-key menumap [compose-new] '("Compose new" . mu4e-compose-new))
+
+ (define-key menumap [sepa3] '("--"))
+
+ (define-key menumap [query-next]
+ '("Next query" . mu4e-headers-query-next))
+ (define-key menumap [query-prev] '("Previous query" .
+ mu4e-headers-query-prev))
+ (define-key menumap [narrow-search] '("Narrow search" .
+ mu4e-headers-search-narrow))
+ (define-key menumap [bookmark] '("Search bookmark" .
+ mu4e-headers-search-bookmark))
+ (define-key menumap [jump] '("Jump to maildir" .
+ mu4e~headers-jump-to-maildir))
+ (define-key menumap [refresh] '("Refresh" . mu4e-search-rerun))
+ (define-key menumap [search] '("Search" . mu4e-headers-search))
+
+ (define-key menumap [sepa4] '("--"))
+
+ (define-key menumap [view] '("View" . mu4e-headers-view-message))
+ (define-key menumap [next] '("Next" . mu4e-headers-next))
+ (define-key menumap [previous] '("Previous" . mu4e-headers-prev))
+ (define-key menumap [sepa5] '("--")))
+ map)))
+(fset 'mu4e-headers-mode-map mu4e-headers-mode-map)
+
+(defun mu4e~header-line-format ()
+ "Get the format for the header line."
+ (let ((uparrow (if mu4e-use-fancy-chars " ▲" " ^"))
+ (downarrow (if mu4e-use-fancy-chars " ▼" " V")))
+ (cons
+ (make-string
+ (+ mu4e--mark-fringe-len (floor (fringe-columns 'left t))) ?\s)
+ (mapcar
+ (lambda (item)
+ (let* ( ;; with threading enabled, we're necessarily sorting by date.
+ (sort-field (if mu4e-search-threads :date mu4e-headers-sort-field))
+ (field (car item)) (width (cdr item))
+ (info (cdr (assoc field
+ (append mu4e-header-info mu4e-header-info-custom))))
+ (sortable (plist-get info :sortable))
+ ;; if sortable, it is either t (when field is sortable itself)
+ ;; or a symbol (if another field is used for sorting)
+ (this-field (when sortable (if (booleanp sortable) field sortable)))
+ (help (plist-get info :help))
+ ;; triangle to mark the sorted-by column
+ (arrow
+ (when (and sortable (eq this-field sort-field))
+ (if (eq mu4e-headers-sort-direction 'descending) downarrow uparrow)))
+ (name (concat (plist-get info :shortname) arrow))
+ (map (make-sparse-keymap)))
+ (when sortable
+ (define-key map [header-line mouse-1]
+ (lambda (&optional e)
+ ;; getting the field, inspired by `tabulated-list-col-sort'
+ (interactive "e")
+ (let* ((obj (posn-object (event-start e)))
+ (field
+ (and obj (get-text-property 0 'field (car obj)))))
+ ;; "t": if we're already sorted by field, the sort-order is
+ ;; changed
+ (mu4e-headers-change-sorting field t)))))
+ (concat
+ (propertize
+ (if width
+ (truncate-string-to-width name width 0 ?\s truncate-string-ellipsis)
+ name)
+ 'face (when arrow 'bold)
+ 'help-echo help
+ 'mouse-face (when sortable 'highlight)
+ 'keymap (when sortable map)
+ 'field field) " ")))
+ mu4e-headers-fields))))
+
+(defun mu4e~headers-maybe-auto-update ()
+ "Update the current headers buffer after indexing has brought
+some changes, `mu4e-headers-auto-update' is non-nil and there is
+no user-interaction ongoing."
+ (when (and mu4e-headers-auto-update ;; must be set
+ mu4e-index-update-status
+ (not (zerop (plist-get mu4e-index-update-status :updated)))
+ (zerop (mu4e-mark-marks-num)) ;; non active marks
+ (not (active-minibuffer-window))) ;; no user input only
+ ;; rerun search if there's a live window with search results;
+ ;; otherwise we'd trigger a headers view from out of nowhere.
+ (when (and (buffer-live-p (mu4e-get-headers-buffer))
+ (window-live-p (get-buffer-window (mu4e-get-headers-buffer) t)))
+ (mu4e-search-rerun))))
+
+(defcustom mu4e-headers-eldoc-format "“%s” from %f on %d"
+ "Format for the `eldoc' string for the current message in the headers buffer.
+The following specs are supported:
+- %s: the message Subject
+- %f: the message From
+- %t: the message To
+- %c: the message Cc
+- %d: the message Date
+- %p: the message priority
+- %m: the maildir containing the message
+- %F: the message’s flags
+- %M: the Message-Id"
+ :type 'string
+ :group 'mu4e-headers)
+
+(defun mu4e-headers-eldoc-function (&rest _args)
+ (let ((msg (get-text-property (point) 'msg)))
+ (when msg
+ (format-spec
+ mu4e-headers-eldoc-format
+ `((?s . ,(mu4e-message-field msg :subject))
+ (?f . ,(mu4e~headers-contact-str (mu4e-message-field msg :from)))
+ (?t . ,(mu4e~headers-contact-str (mu4e-message-field msg :to)))
+ (?c . ,(mu4e~headers-contact-str (mu4e-message-field msg :cc)))
+ (?d . ,(mu4e~headers-human-date msg))
+ (?p . ,(mu4e-message-field msg :priority))
+ (?m . ,(mu4e-message-field msg :maildir))
+ (?F . ,(mu4e-message-field msg :flags))
+ (?M . ,(mu4e-message-field msg :message-id)))))))
+
+(define-derived-mode mu4e-headers-mode special-mode
+ "mu4e:headers"
+ "Major mode for displaying mu4e search results.
+\\{mu4e-headers-mode-map}."
+ (use-local-map mu4e-headers-mode-map)
+ (make-local-variable 'mu4e~headers-proc)
+ (make-local-variable 'mu4e~highlighted-docid)
+ (set (make-local-variable 'hl-line-face) 'mu4e-header-highlight-face)
+
+ ;; Eldoc support
+ (when (featurep 'eldoc)
+ (if (boundp 'eldoc-documentation-functions)
+ ;; Emacs 28 or newer
+ (add-hook 'eldoc-documentation-functions
+ #'mu4e-headers-eldoc-function nil t)
+ ;; Emacs 27 or older
+ (when (fboundp 'add-function) ;; add-function was added in 24.4.
+ (add-function :before-until (local 'eldoc-documentation-function)
+ #'mu4e-headers-eldoc-function))))
+
+ ;; support bookmarks.
+ (set (make-local-variable 'bookmark-make-record-function)
+ 'mu4e--make-bookmark-record)
+ ;; maybe update the current headers upon indexing changes
+ (add-hook 'mu4e-index-updated-hook #'mu4e~headers-maybe-auto-update)
+ (setq
+ truncate-lines t
+ buffer-undo-list t ;; don't record undo information
+ overwrite-mode nil
+ header-line-format (mu4e~header-line-format))
+
+ (mu4e--mark-initialize) ;; initialize the marking subsystem
+ (mu4e-context-minor-mode)
+ (mu4e-update-minor-mode)
+ (mu4e-search-minor-mode)
+ (hl-line-mode 1))
+
+;;; Highlighting
+
+(defvar mu4e~highlighted-docid nil
+ "The highlighted docid")
+
+(defun mu4e~headers-highlight (docid)
+ "Highlight the header with DOCID, or do nothing if it's not found.
+Also, unhighlight any previously highlighted headers."
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (save-excursion
+ ;; first, unhighlight the previously highlighted docid, if any
+ (when (and docid mu4e~highlighted-docid
+ (mu4e~headers-goto-docid mu4e~highlighted-docid))
+ (hl-line-unhighlight))
+ ;; now, highlight the new one
+ (when (mu4e~headers-goto-docid docid)
+ (hl-line-highlight)))
+ (setq mu4e~highlighted-docid docid)))
+
+;;; Misc 2
+
+(defun mu4e~headers-select-window ()
+ "When there is a visible window for the headers buffer, make sure
+to select it. This is needed when adding new headers, otherwise
+adding a lot of new headers looks really choppy."
+ (let ((win (get-buffer-window (mu4e-get-headers-buffer))))
+ (when win (select-window win))))
+
+(defun mu4e-headers-goto-message-id (msgid)
+ "Go to the next message with message-id MSGID. Return the
+message plist, or nil if not found."
+ (mu4e-headers-find-if
+ (lambda (msg)
+ (let ((this-msgid (mu4e-message-field msg :message-id)))
+ (when (and this-msgid (string= msgid this-msgid))
+ msg)))))
+
+;;; Marking 2
+
+(defun mu4e~headers-mark (docid mark)
+ "(Visually) mark the header for DOCID with character MARK."
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (let ((inhibit-read-only t) (oldpoint (point)))
+ (unless (mu4e~headers-goto-docid docid)
+ (mu4e-error "Cannot find message with docid %S" docid))
+ ;; now, we're at the beginning of the header, looking at
+ ;; <docid>\004
+ ;; (which is invisible). jump past that…
+ (unless (re-search-forward mu4e~headers-docid-post nil t)
+ (mu4e-error "Cannot find the `mu4e~headers-docid-post' separator"))
+
+ ;; clear old marks, and add the new ones.
+ (let ((msg (get-text-property (point) 'msg)))
+ (delete-char mu4e--mark-fringe-len)
+ (insert (propertize
+ (format mu4e--mark-fringe-format mark)
+ 'face 'mu4e-header-marks-face
+ 'docid docid
+ 'msg msg)))
+ (goto-char oldpoint))))
+
+
+;;; Queries & searching
+(defvar mu4e~headers-mode-line-label "")
+(defun mu4e~headers-update-mode-line ()
+ "Update mode-line settings."
+ (let* ((flagstr
+ (mapconcat
+ (lambda (flag-cell)
+ (if (car flag-cell)
+ (if mu4e-use-fancy-chars
+ (cddr flag-cell) (cadr flag-cell) ) ""))
+ `((,mu4e-search-full . ,mu4e-headers-full-label)
+ (,mu4e-headers-include-related . ,mu4e-headers-related-label)
+ (,mu4e-search-threads . ,mu4e-headers-threaded-label)
+ (,mu4e-headers-skip-duplicates . ,mu4e-headers-skip-duplicates-label))
+ ""))
+ (name "mu4e-headers"))
+
+ (setq mode-name name)
+ (setq mu4e~headers-mode-line-label (concat flagstr " " mu4e--search-last-query))
+
+ (make-local-variable 'global-mode-string)
+
+ (add-to-list 'global-mode-string
+ `(:eval
+ (concat
+ (propertize
+ (mu4e-quote-for-modeline ,mu4e~headers-mode-line-label)
+ 'face 'mu4e-modeline-face)
+ " "
+ (if (and mu4e-display-update-status-in-modeline
+ (buffer-live-p mu4e--update-buffer)
+ (process-live-p (get-buffer-process
+ mu4e--update-buffer)))
+ (propertize " (updating)" 'face 'mu4e-modeline-face)
+ ""))))))
+
+
+(defun mu4e~headers-redraw-get-view-window ()
+ "Close all windows, redraw the headers buffer based on the value
+of `mu4e-split-view', and return a window for the message view."
+ (if (eq mu4e-split-view 'single-window)
+ (or (and (buffer-live-p (mu4e-get-view-buffer))
+ (get-buffer-window (mu4e-get-view-buffer)))
+ (selected-window))
+ (mu4e-hide-other-mu4e-buffers)
+ (unless (buffer-live-p (mu4e-get-headers-buffer))
+ (mu4e-error "No headers buffer available"))
+ (switch-to-buffer (mu4e-get-headers-buffer))
+ ;; kill the existing view buffer
+ (when (buffer-live-p (mu4e-get-view-buffer))
+ (kill-buffer (mu4e-get-view-buffer)))
+ ;; get a new view window
+ (setq mu4e~headers-view-win
+ (with-demoted-errors "Unable to split window: %S"
+ (cond
+ ((eq mu4e-split-view 'horizontal) ;; split horizontally
+ (split-window-vertically mu4e-headers-visible-lines))
+ ((eq mu4e-split-view 'vertical) ;; split vertically
+ (split-window-horizontally mu4e-headers-visible-columns))
+ ((functionp mu4e-split-view)
+ (funcall mu4e-split-view))
+ (t ;; no splitting; just use the currently selected one
+ (selected-window)))))))
+
+;;; Search-based marking
+
+(defun mu4e-headers-for-each (func)
+ "Call FUNC for each header, moving point to the header.
+FUNC receives one argument, the message s-expression for the
+corresponding header."
+ (save-excursion
+ (goto-char (point-min))
+ (while (search-forward mu4e~headers-docid-pre nil t)
+ ;; not really sure why we need to jump to bol; we do need to, otherwise we
+ ;; miss lines sometimes...
+ (let ((msg (get-text-property (line-beginning-position) 'msg)))
+ (when msg
+ (funcall func msg))))))
+
+(defun mu4e-headers-find-if (func &optional backward)
+ "Move to the next header for which FUNC returns non-`nil',
+starting from the current position. FUNC receives one argument, the
+message s-expression for the corresponding header. If BACKWARD is
+non-`nil', search backwards. Returns the new position, or `nil' if
+nothing was found. If you want to exclude matches for the current
+message, you can use `mu4e-headers-find-if-next'."
+ (let ((pos)
+ (search-func (if backward 'search-backward 'search-forward)))
+ (save-excursion
+ (while (and (null pos)
+ (funcall search-func mu4e~headers-docid-pre nil t))
+ ;; not really sure why we need to jump to bol; we do need to, otherwise
+ ;; we miss lines sometimes...
+ (let ((msg (get-text-property (line-beginning-position) 'msg)))
+ (when (and msg (funcall func msg))
+ (setq pos (point))))))
+ (when pos
+ (goto-char pos))))
+
+(defun mu4e-headers-find-if-next (func &optional backwards)
+ "Like `mu4e-headers-find-if', but do not match the current header.
+Move to the next or (if BACKWARDS is non-`nil') header for which FUNC
+returns non-`nil', starting from the current position."
+ (let ((pos))
+ (save-excursion
+ (if backwards
+ (beginning-of-line)
+ (end-of-line))
+ (setq pos (mu4e-headers-find-if func backwards)))
+ (when pos (goto-char pos))))
+
+(defvar mu4e~headers-regexp-hist nil
+ "History list of regexps used.")
+
+(defun mu4e-headers-mark-for-each-if (markpair mark-pred &optional param)
+ "Mark all headers for which predicate function MARK-PRED returns
+non-nil with MARKPAIR. MARK-PRED is function that receives two
+arguments, MSG (the message at point) and PARAM (a user-specified
+parameter). MARKPAIR is a cell (MARK . TARGET); see
+`mu4e-mark-at-point' for details about marks."
+ (mu4e-headers-for-each
+ (lambda (msg)
+ (when (funcall mark-pred msg param)
+ (mu4e-mark-at-point (car markpair) (cdr markpair))))))
+
+(defun mu4e-headers-mark-pattern ()
+ "Ask user for a kind of mark (move, delete etc.), a field to
+match and a regular expression to match with. Then, mark all
+matching messages with that mark."
+ (interactive)
+ (let ((markpair (mu4e--mark-get-markpair "Mark matched messages with: " t))
+ (field (mu4e-read-option "Field to match: "
+ '( ("subject" . :subject)
+ ("from" . :from)
+ ("to" . :to)
+ ("cc" . :cc)
+ ("bcc" . :bcc)
+ ("list" . :list))))
+ (pattern (read-string
+ (mu4e-format "Regexp:")
+ nil 'mu4e~headers-regexp-hist)))
+ (mu4e-headers-mark-for-each-if
+ markpair
+ (lambda (msg _param)
+ (let* ((value (mu4e-msg-field msg field)))
+ (if (member field '(:to :from :cc :bcc :reply-to))
+ (cl-find-if (lambda (contact)
+ (let ((name (mu4e-contact-name contact))
+ (email (mu4e-contact-email contact)))
+ (or (and name (string-match pattern name))
+ (and email (string-match pattern email))))) value)
+ (string-match pattern (or value ""))))))))
+
+(defun mu4e-headers-mark-custom ()
+ "Mark messages based on a user-provided predicate function."
+ (interactive)
+ (let* ((pred (mu4e-read-option "Match function: "
+ mu4e-headers-custom-markers))
+ (param (when (cdr pred) (eval (cdr pred))))
+ (markpair (mu4e--mark-get-markpair "Mark matched messages with: " t)))
+ (mu4e-headers-mark-for-each-if markpair (car pred) param)))
+
+(defun mu4e~headers-get-thread-info (msg what)
+ "Get WHAT (a symbol, either path or thread-id) for MSG."
+ (let* ((meta (or (mu4e-message-field msg :meta)
+ (mu4e-error "No thread info found")))
+ (path (or (plist-get meta :path)
+ (mu4e-error "No threadpath found"))))
+ (cl-case what
+ (path path)
+ (thread-id
+ (save-match-data
+ ;; the thread id is the first segment of the thread path
+ (when (string-match "^\\([[:xdigit:]]+\\):?" path)
+ (match-string 1 path))))
+ (otherwise (mu4e-error "Not supported")))))
+
+(defun mu4e-headers-mark-thread-using-markpair (markpair &optional subthread)
+ "Mark the thread at point using the given markpair. If SUBTHREAD is
+non-nil, marking is limited to the message at point and its
+descendants."
+ (let* ((mark (car markpair))
+ (allowed-marks (mapcar 'car mu4e-marks)))
+ (unless (memq mark allowed-marks)
+ (mu4e-error "The mark (%s) has to be one of: %s"
+ mark allowed-marks)))
+ ;; note: the thread id is shared by all messages in a thread
+ (let* ((msg (mu4e-message-at-point))
+ (thread-id (mu4e~headers-get-thread-info msg 'thread-id))
+ (path (mu4e~headers-get-thread-info msg 'path))
+ ;; the thread path may have a ':z' suffix for sorting;
+ ;; remove it for subthread matching.
+ (match-path (replace-regexp-in-string ":z$" "" path))
+ (last-marked-point))
+ (mu4e-headers-for-each
+ (lambda (cur-msg)
+ (let ((cur-thread-id (mu4e~headers-get-thread-info cur-msg 'thread-id))
+ (cur-thread-path (mu4e~headers-get-thread-info cur-msg 'path)))
+ (if subthread
+ ;; subthread matching; mymsg's thread path should have path as its
+ ;; prefix
+ (when (string-match (concat "^" match-path) cur-thread-path)
+ (mu4e-mark-at-point (car markpair) (cdr markpair))
+ (setq last-marked-point (point)))
+ ;; nope; not looking for the subthread; looking for the whole thread
+ (when (string= thread-id cur-thread-id)
+ (mu4e-mark-at-point (car markpair) (cdr markpair))
+ (setq last-marked-point (point)))))))
+ (when last-marked-point
+ (goto-char last-marked-point)
+ (mu4e-headers-next))))
+
+(defun mu4e-headers-mark-thread (&optional subthread markpair)
+ "Like `mu4e-headers-mark-thread-using-markpair' but prompt for the markpair."
+ (interactive
+ (let* ((subthread current-prefix-arg))
+ (list current-prefix-arg
+ ;; FIXME: e.g., for refiling we should evaluate this
+ ;; for each line separately
+ (mu4e--mark-get-markpair
+ (if subthread "Mark subthread with: " "Mark whole thread with: ")
+ t))))
+ (mu4e-headers-mark-thread-using-markpair markpair subthread))
+
+(defun mu4e-headers-mark-subthread (&optional markpair)
+ "Like `mu4e-mark-thread', but only for a sub-thread."
+ (interactive)
+ (if markpair (mu4e-headers-mark-thread t markpair)
+ (let ((current-prefix-arg t))
+ (call-interactively 'mu4e-headers-mark-thread))))
+
+\f
+;;; Interactive functions
+(defun mu4e-headers-change-sorting (&optional field dir)
+ "Change the sorting/threading parameters.
+FIELD is the field to sort by; DIR is a symbol: either
+`ascending', `descending', t (meaning: if FIELD is the same as
+the current sortfield, change the sort-order) or nil (ask the
+user)."
+ (interactive)
+ (let* ((field
+ (or field
+ (mu4e-read-option "Sortfield: " mu4e~headers-sort-field-choices)))
+ ;; note: 'sortable' is either a boolean (meaning: if non-nil, this is
+ ;; sortable field), _or_ another field (meaning: sort by this other field).
+ (sortable (plist-get (cdr (assoc field mu4e-header-info)) :sortable))
+ ;; error check
+ (sortable
+ (if sortable
+ sortable
+ (mu4e-error "Not a sortable field")))
+ (sortfield (if (booleanp sortable) field sortable))
+ (dir
+ (cl-case dir
+ ((ascending descending) dir)
+ ;; change the sort order if field = curfield
+ (t
+ (if (eq sortfield mu4e-headers-sort-field)
+ (if (eq mu4e-headers-sort-direction 'ascending)
+ 'descending 'ascending)
+ 'descending)))))
+ (setq
+ mu4e-headers-sort-field sortfield
+ mu4e-headers-sort-direction dir)
+ (mu4e-message "Sorting by %s (%s)"
+ (symbol-name sortfield)
+ (symbol-name mu4e-headers-sort-direction))
+ (mu4e-search-rerun)))
+
+
+(defun mu4e-headers-toggle-setting (&optional dont-refresh)
+ "Toggle some aspect of headers display.
+When prefix-argument DONT-REFRESH is non-nill, do not refresh the
+last search with the new setting."
+ (interactive "P")
+ (let* ((toggles '(("fFull-search" . mu4e-search-full)
+ ("rInclude-related" . mu4e-headers-include-related)
+ ("tShow threads" . mu4e-search-threads)
+ ("uSkip duplicates" . mu4e-headers-skip-duplicates)))
+ (toggles (seq-map
+ (lambda (cell)
+ (cons
+ (concat (car cell) (format" (%s)"
+ (if (symbol-value (cdr cell)) "on" "off")))
+ (cdr cell))) toggles))
+ (choice (mu4e-read-option "Toggle setting " toggles)))
+ (when choice
+ (set choice (not (symbol-value choice)))
+ (mu4e-message "Set `%s' to %s" (symbol-name choice) (symbol-value choice))
+ (unless dont-refresh
+ (mu4e-search-rerun)))))
+
+
+(defun mu4e~headers-toggle (name togglevar dont-refresh)
+ "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil,
+re-run the last search."
+ (set togglevar (not (symbol-value togglevar)))
+ (mu4e-message "%s turned %s%s"
+ name
+ (if (symbol-value togglevar) "on" "off")
+ (if dont-refresh
+ " (press 'g' to refresh)" ""))
+ (unless dont-refresh
+ (mu4e-search-rerun)))
+
+(defun mu4e-headers-toggle-threading (&optional dont-refresh)
+ "Toggle `mu4e-search-threads'. With prefix-argument, do
+_not_ refresh the last search with the new setting for threading."
+ (interactive "P")
+ (mu4e~headers-toggle "Threading" 'mu4e-search-threads dont-refresh))
+
+(defun mu4e-headers-toggle-full-search (&optional dont-refresh)
+ "Toggle `mu4e-search-full'. With prefix-argument, do
+_not_ refresh the last search with the new setting for threading."
+ (interactive "P")
+ (mu4e~headers-toggle "Full-search"
+ 'mu4e-search-full dont-refresh))
+
+(defun mu4e-headers-toggle-include-related (&optional dont-refresh)
+ "Toggle `mu4e-headers-include-related'. With prefix-argument, do
+_not_ refresh the last search with the new setting for threading."
+ (interactive "P")
+ (mu4e~headers-toggle "Include-related"
+ 'mu4e-headers-include-related dont-refresh))
+
+(defun mu4e-headers-toggle-skip-duplicates (&optional dont-refresh)
+ "Toggle `mu4e-headers-skip-duplicates'. With prefix-argument, do
+_not_ refresh the last search with the new setting for threading."
+ (interactive "P")
+ (mu4e~headers-toggle "Skip-duplicates"
+ 'mu4e-headers-skip-duplicates dont-refresh))
+
+(defvar mu4e~headers-loading-buf nil
+ "A buffer for loading a message view.")
+
+(defun mu4e-headers-view-message ()
+ "View message at point .
+If there's an existing window for the view, re-use that one . If
+not, create a new one, depending on the value of
+`mu4e-split-view': if it's a symbol `horizontal' or `vertical',
+split the window accordingly; if it is nil, replace the current
+window . "
+ (interactive)
+ (unless (eq major-mode 'mu4e-headers-mode)
+ (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
+ (let* ((msg (mu4e-message-at-point))
+ (path (mu4e-message-field msg :path))
+ (_exists (or (file-readable-p path)
+ (mu4e-warn "No message at %s" path)))
+ (docid (or (mu4e-message-field msg :docid)
+ (mu4e-warn "No message at point")))
+ (mark-as-read
+ (if (functionp mu4e-view-auto-mark-as-read)
+ (funcall mu4e-view-auto-mark-as-read msg)
+ mu4e-view-auto-mark-as-read))
+ (viewwin (mu4e~headers-redraw-get-view-window)))
+ (unless (window-live-p viewwin)
+ (mu4e-error "Cannot get a message view"))
+ (select-window viewwin)
+
+ ;; show some 'loading...' buffer
+ (unless (buffer-live-p mu4e~headers-loading-buf)
+ (setq mu4e~headers-loading-buf (get-buffer-create " *mu4e-loading*"))
+ (with-current-buffer mu4e~headers-loading-buf
+ (mu4e-loading-mode)))
+
+ (switch-to-buffer mu4e~headers-loading-buf)
+ (mu4e--server-view docid mark-as-read)))
+
+
+(defun mu4e~headers-move (lines)
+ "Move point LINES lines.
+Move foward if LINES is positive or backwards if LINES is
+negative. If this succeeds, return the new docid. Otherwise,
+return nil."
+ (unless (eq major-mode 'mu4e-headers-mode)
+ (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
+ (cl-flet ((goto-next-line
+ (arg)
+ (condition-case _err
+ (prog1
+ (let (line-move-visual)
+ (and (line-move arg) 0))
+ ;; Skip invisible text at BOL possibly hidden by
+ ;; the end of another invisible overlay covering
+ ;; previous EOL.
+ (move-to-column 2))
+ ((beginning-of-buffer end-of-buffer)
+ 1))))
+ (let* ((succeeded (zerop (goto-next-line lines)))
+ (docid (mu4e~headers-docid-at-point)))
+ ;; move point, even if this function is called when this window is not
+ ;; visible
+ (when docid
+ ;; update all windows showing the headers buffer
+ (walk-windows
+ (lambda (win)
+ (when (eq (window-buffer win) (mu4e-get-headers-buffer))
+ (set-window-point win (point))))
+ nil t)
+ (if (eq mu4e-split-view 'single-window)
+ (when (eq (window-buffer) (mu4e-get-view-buffer))
+ (mu4e-headers-view-message))
+ ;; update message view if it was already showing
+ (when (and mu4e-split-view (window-live-p mu4e~headers-view-win))
+ (mu4e-headers-view-message)))
+ ;; attempt to highlight the new line, display the message
+ (mu4e~headers-highlight docid)
+ (if succeeded
+ docid
+ nil)))))
+
+(defun mu4e-headers-next (&optional n)
+ "Move point to the next message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth next
+header."
+ (interactive "P")
+ (mu4e~headers-move (or n 1)))
+
+(defun mu4e-headers-prev (&optional n)
+ "Move point to the previous message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth
+previous header."
+ (interactive "P")
+ (mu4e~headers-move (- (or n 1))))
+
+(defun mu4e~headers-prev-or-next-unread (backwards)
+ "Move point to the next message that is unread (and
+untrashed). If BACKWARDS is non-`nil', move backwards."
+ (interactive)
+ (or (mu4e-headers-find-if-next
+ (lambda (msg)
+ (let ((flags (mu4e-message-field msg :flags)))
+ (and (member 'unread flags) (not (member 'trashed flags)))))
+ backwards)
+ (mu4e-message (format "No %s unread message found"
+ (if backwards "previous" "next")))))
+
+(defun mu4e-headers-prev-unread ()
+ "Move point to the previous message that is unread (and
+untrashed)."
+ (interactive)
+ (mu4e~headers-prev-or-next-unread t))
+
+(defun mu4e-headers-next-unread ()
+ "Move point to the next message that is unread (and
+untrashed)."
+ (interactive)
+ (mu4e~headers-prev-or-next-unread nil))
+
+(defun mu4e~headers-jump-to-maildir (maildir &optional edit)
+ "Show the messages in maildir.
+The user is prompted to ask what maildir. If prefix arg EDIT is
+given, offer to edit the search query before executing it."
+ (interactive
+ (let ((maildir (mu4e-ask-maildir "Jump to maildir: ")))
+ (list maildir current-prefix-arg)))
+ (when maildir
+ (let* ((query (format "maildir:\"%s\"" maildir))
+ (query (if edit (mu4e-search-read-query "Refine query: " query) query)))
+ (mu4e-mark-handle-when-leaving)
+ (mu4e-search query))))
+
+(defun mu4e-headers-split-view-grow (&optional n)
+ "In split-view, grow the headers window.
+In horizontal split-view, increase the number of lines shown by N.
+In vertical split-view, increase the number of columns shown by N.
+If N is negative shrink the headers window. When not in split-view
+do nothing."
+ (interactive "P")
+ (let ((n (or n 1))
+ (hwin (get-buffer-window (mu4e-get-headers-buffer))))
+ (when (and (buffer-live-p (mu4e-get-view-buffer)) (window-live-p hwin))
+ (let ((n (or n 1)))
+ (cl-case mu4e-split-view
+ ;; emacs has weird ideas about what horizontal, vertical means...
+ (horizontal
+ (window-resize hwin n nil)
+ (cl-incf mu4e-headers-visible-lines n))
+ (vertical
+ (window-resize hwin n t)
+ (cl-incf mu4e-headers-visible-columns n)))))))
+
+(defun mu4e-headers-split-view-shrink (&optional n)
+ "In split-view, shrink the headers window.
+In horizontal split-view, decrease the number of lines shown by N.
+In vertical split-view, decrease the number of columns shown by N.
+If N is negative grow the headers window. When not in split-view
+do nothing."
+ (interactive "P")
+ (mu4e-headers-split-view-grow (- (or n 1))))
+
+(defun mu4e-headers-action (&optional actionfunc)
+ "Ask user what to do with message-at-point, then do it.
+The actions are specified in `mu4e-headers-actions'. Optionally,
+pass ACTIONFUNC, which is a function that takes a msg-plist
+argument."
+ (interactive)
+ (let ((msg (mu4e-message-at-point))
+ (afunc (or actionfunc (mu4e-read-option "Action: " mu4e-headers-actions))))
+ (funcall afunc msg)))
+
+(defun mu4e-headers-mark-and-next (mark)
+ "Set mark MARK on the message at point or on all messages in the
+region if there is a region, then move to the next message."
+ (interactive)
+ (mu4e-mark-set mark)
+ (when mu4e-headers-advance-after-mark (mu4e-headers-next)))
+
+(defun mu4e~headers-quit-buffer ()
+ "Quit the mu4e-headers buffer.
+This is a rather complex function, to ensure we don't disturb
+other windows."
+ (interactive)
+ (if (eq mu4e-split-view 'single-window)
+ (progn (mu4e-mark-handle-when-leaving)
+ (kill-buffer))
+ (unless (eq major-mode 'mu4e-headers-mode)
+ (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
+ (mu4e-mark-handle-when-leaving)
+ (let ((curbuf (current-buffer))
+ (curwin (selected-window)))
+ (walk-windows
+ (lambda (win)
+ (with-selected-window win
+ ;; if we the view window connected to this one, kill it
+ (when (and (not (one-window-p win)) (eq mu4e~headers-view-win win))
+ (delete-window win)
+ (setq mu4e~headers-view-win nil)))
+ ;; and kill any _other_ (non-selected) window that shows the current
+ ;; buffer
+ (when (and
+ (eq curbuf (window-buffer win)) ;; does win show curbuf?
+ (not (eq curwin win)) ;; it's not the curwin?
+ (not (one-window-p))) ;; and not the last one?
+ (delete-window win)))) ;; delete it!
+ ;; now, all *other* windows should be gone. kill ourselves, and return
+ ;; to the main view
+ (kill-buffer)
+ (mu4e--main-view 'refresh))))
+
+\f
+;;; Loading messages
+;;
+(defvar mu4e-loading-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map "n" #'ignore)
+ (define-key map "p" #'ignore)
+ (define-key map "q" #'bury-buffer)
+ map)
+ "Keymap for *mu4e-loading* buffers.")
+
+(define-derived-mode mu4e-loading-mode special-mode
+ "mu4e:loading"
+ (use-local-map mu4e-loading-mode-map)
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert (propertize "Loading message..."
+ 'face 'mu4e-system-face 'intangible t))))
+
+(defun mu4e~loading-close ()
+ "Bury the mu4e Loading... buffer, if any."
+ (let* ((buf mu4e~headers-loading-buf)
+ (win (and (buffer-live-p buf) (get-buffer-window buf t))))
+ (when (window-live-p win)
+ (delete-window win))))
+
+(provide 'mu4e-headers)
+;;; mu4e-headers.el ends here
--- /dev/null
+;;; mu4e-helpers.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Helper functions used in the mu4e. This is slowly usurp all the code from
+;; mu4e-utils.el that does not depend on other parts of mu4e.
+
+;;; Code:
+
+(require 'seq)
+(require 'ido)
+(require 'cl-lib)
+(require 'bookmark)
+
+(require 'mu4e-config)
+\f
+;;; Customization
+
+(defcustom mu4e-debug nil
+ "When set to non-nil, log debug information to the mu4e log buffer."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-modeline-max-width 42
+ "Determines the maximum length of the modeline string.
+If the string exceeds this limit, it will be truncated to fit."
+ :type 'integer
+ :group 'mu4e)
+
+(defcustom mu4e-completing-read-function 'ido-completing-read
+ "Function to be used to receive user-input during completion.
+Suggested possible values are:
+ * `completing-read': built-in completion method
+ * `ido-completing-read': dynamic completion within the minibuffer."
+ :type 'function
+ :options '(completing-read ido-completing-read)
+ :group 'mu4e)
+
+(defcustom mu4e-use-fancy-chars nil
+ "When set, allow fancy (Unicode) characters for marks/threads.
+You can customize the exact fancy characters used with
+`mu4e-marks' and various `mu4e-headers-..-mark' and
+`mu4e-headers..-prefix' variables."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-display-update-status-in-modeline nil
+ "Non-nil value will display the update status in the modeline."
+ :group 'mu4e
+ :type 'boolean)
+
+;; maybe move the next ones... but they're convenient
+;; here because they're needed in multiple buffers.
+
+(defcustom mu4e-view-auto-mark-as-read t
+ "Automatically mark messages as read when you read them.
+This is the default behavior, but can be turned off, for example
+when using a read-only file-system.
+
+This can also be set to a function; if so, receives a message
+plist which should evaluate to nil if the message should *not* be
+marked as read-only, or non-nil otherwise."
+ :type '(choice
+ boolean
+ function)
+ :group 'mu4e-view)
+
+
+(defcustom mu4e-split-view 'horizontal
+ "How to show messages / headers.
+A symbol which is either:
+ * `horizontal': split horizontally (headers on top)
+ * `vertical': split vertically (headers on the left).
+ * `single-window': view and headers in one window (mu4e will try not to
+ touch your window layout), main view in minibuffer
+ * a function: the function is responsible to return some window for
+ the view.
+ * anything else: don't split (show either headers or messages,
+ not both).
+Also see `mu4e-headers-visible-lines'
+and `mu4e-headers-visible-columns'."
+ :type '(choice (const :tag "Split horizontally" horizontal)
+ (const :tag "Split vertically" vertical)
+ (const :tag "Single window" single-window)
+ (const :tag "Don't split" nil))
+ :group 'mu4e-headers)
+\f
+;;; Buffers
+
+(defconst mu4e-main-buffer-name " *mu4e-main*"
+ "Name of the mu4e main buffer.
+The default name starts with SPC and therefore is not visible in
+buffer list.")
+(defconst mu4e-headers-buffer-name "*mu4e-headers*"
+ "Name of the buffer for message headers.")
+(defconst mu4e-embedded-buffer-name " *mu4e-embedded*"
+ "Name for the embedded message view buffer.")
+(defconst mu4e-view-buffer-name "*Article*"
+ "Name of the view buffer.")
+
+(defun mu4e-get-headers-buffer ()
+ "Get the buffer object from `mu4e-headers-buffer-name'."
+ (get-buffer mu4e-headers-buffer-name))
+
+(defun mu4e-get-view-buffer ()
+ "Get the buffer object from `mu4e-view-buffer-name'."
+ (get-buffer mu4e-view-buffer-name))
+
+(defun mu4e-select-other-view ()
+ "Switch between headers view and message view."
+ (interactive)
+ (let* ((other-buf
+ (cond
+ ((eq major-mode 'mu4e-headers-mode)
+ (mu4e-get-view-buffer))
+ ((eq major-mode 'mu4e-view-mode)
+ (mu4e-get-headers-buffer))))
+ (other-win (and other-buf (get-buffer-window other-buf))))
+ (if (window-live-p other-win)
+ (select-window other-win)
+ (mu4e-message "No window to switch to"))))
+
+\f
+;;; Windows
+(defun mu4e-hide-other-mu4e-buffers ()
+ "Bury mu4e buffers.
+Hide (main, headers, view) (and delete all windows displaying
+it). Do _not_ bury the current buffer, though."
+ (interactive)
+ (unless (eq mu4e-split-view 'single-window)
+ (let ((curbuf (current-buffer)))
+ ;; note: 'walk-windows' does not seem to work correctly when modifying
+ ;; windows; therefore, the doloops here
+ (dolist (frame (frame-list))
+ (dolist (win (window-list frame nil))
+ (with-current-buffer (window-buffer win)
+ (unless (eq curbuf (current-buffer))
+ (when (member major-mode '(mu4e-headers-mode mu4e-view-mode))
+ (when (eq t (window-deletable-p win))
+ (delete-window win))))))) t)))
+\f
+;;; Modeline
+
+(defun mu4e-quote-for-modeline (str)
+ "Quote STR to be used literally in the modeline.
+The string will be shortened to fit if its length exceeds
+`mu4e-modeline-max-width'."
+ (replace-regexp-in-string
+ "%" "%%"
+ (truncate-string-to-width str mu4e-modeline-max-width 0 nil t)))
+
+
+\f
+;;; Messages, warnings and errors
+(defun mu4e-format (frm &rest args)
+ "Create [mu4e]-prefixed string based on format FRM and ARGS."
+ (concat
+ "[" (propertize "mu4e" 'face 'mu4e-title-face) "] "
+ (apply 'format frm
+ (mapcar (lambda (x)
+ (if (stringp x)
+ (decode-coding-string x 'utf-8)
+ x))
+ args))))
+
+(defun mu4e-message (frm &rest args)
+ "Display FRM with ARGS like `message' in mu4e style.
+If we're waiting for user-input or if there's some message in the
+echo area, don't show anything."
+ (unless (or (active-minibuffer-window))
+ (message "%s" (apply 'mu4e-format frm args))))
+
+(declare-function mu4e~loading-close "mu4e-headers")
+
+(defun mu4e-error (frm &rest args)
+ "Display an error with FRM and ARGS like `mu4e-message'.
+
+Create [mu4e]-prefixed error based on format FRM and ARGS. Does a
+local-exit and does not return, and raises a
+debuggable (backtrace) error."
+ (mu4e-log 'error (apply 'mu4e-format frm args))
+ ;; opportunistically close the "loading" window.
+ (mu4e~loading-close)
+ (error "%s" (apply 'mu4e-format frm args)))
+
+(defun mu4e-warn (frm &rest args)
+ "Create [mu4e]-prefixed warning based on format FRM and ARGS.
+Does a local-exit and does not return."
+ (mu4e-log 'error (apply 'mu4e-format frm args))
+ (user-error "%s" (apply 'mu4e-format frm args)))
+
+;;; Reading user input
+
+(defun mu4e--read-char-choice (prompt choices)
+ "Read and return one of CHOICES, prompting for PROMPT.
+Any input that is not one of CHOICES is ignored. This is mu4e's
+version of `read-char-choice' which becomes case-insentive after
+trying an exact match."
+ (let ((choice) (chosen) (inhibit-quit nil))
+ (while (not chosen)
+ (message nil);; this seems needed...
+ (setq choice (read-char-exclusive prompt))
+ (if (eq choice 27) (keyboard-quit)) ;; quit if ESC is pressed
+ (setq chosen (or (member choice choices)
+ (member (downcase choice) choices)
+ (member (upcase choice) choices))))
+ (car chosen)))
+
+(defun mu4e-read-option (prompt options)
+ "Ask user for an option from a list on the input area.
+PROMPT describes a multiple-choice question to the user. OPTIONS
+describe the options, and is a list of cells describing
+particular options. Cells have the following structure:
+
+ (OPTIONSTRING . RESULT)
+
+where OPTIONSTRING is a non-empty string describing the
+option. The first character of OPTIONSTRING is used as the
+shortcut, and obviously all shortcuts must be different, so you
+can prefix the string with an uniquifying character.
+
+The options are provided as a list for the user to choose from;
+user can then choose by typing CHAR. Example:
+ (mu4e-read-option \"Choose an animal: \"
+ \='((\"Monkey\" . monkey) (\"Gnu\" . gnu) (\"xMoose\" . moose)))
+
+User now will be presented with a list: \"Choose an animal:
+ [M]onkey, [G]nu, [x]Moose\".
+
+Function returns the cdr of the list element."
+ (let* ((prompt (mu4e-format "%s" prompt))
+ (optionsstr
+ (mapconcat
+ (lambda (option)
+ ;; try to detect old-style options, and warn
+ (when (characterp (car-safe (cdr-safe option)))
+ (mu4e-error
+ (concat "Please use the new format for options/actions; "
+ "see the manual")))
+ (let ((kar (substring (car option) 0 1)))
+ (concat
+ "[" (propertize kar 'face 'mu4e-highlight-face) "]"
+ (substring (car option) 1))))
+ options ", "))
+ (response
+ (mu4e--read-char-choice
+ (concat prompt optionsstr
+ " [" (propertize "C-g" 'face 'mu4e-highlight-face)
+ " to cancel]")
+ ;; the allowable chars
+ (seq-map (lambda(elm) (string-to-char (car elm))) options)))
+ (chosen
+ (seq-find
+ (lambda (option) (eq response (string-to-char (car option))))
+ options)))
+ (if chosen
+ (cdr chosen)
+ (mu4e-warn "Unknown shortcut '%c'" response))))
+
+
+\f
+;;; Server properties
+(defvar mu4e--server-props nil
+ "Metadata we receive from the mu4e server.")
+
+(defun mu4e-server-properties ()
+ "Get the server metadata plist."
+ mu4e--server-props)
+
+(defun mu4e-root-maildir()
+ "Get the root maildir."
+ (or (and mu4e--server-props
+ (plist-get mu4e--server-props :root-maildir))
+ (mu4e-error "Root maildir unknown; did you start mu4e?")))
+
+(defun mu4e-database-path()
+ "Get the root maildir."
+ (or (and mu4e--server-props
+ (plist-get mu4e--server-props :database-path))
+ (mu4e-error "Root maildir unknown; did you start mu4e?")))
+
+(defun mu4e-server-version()
+ "Get the root maildir."
+ (or (and mu4e--server-props
+ (plist-get mu4e--server-props :version))
+ (mu4e-error "Version unknown; did you start mu4e?")))
+
+(defun mu4e-last-query-results ()
+ "Get the results (counts) of the last cached queries.
+
+The cached queries are the bookmark / maildir queries that are
+used to populated the read/unread counts in the main view. They
+are refreshed when calling `(mu4e)', i.e., when going to the main
+view.
+
+The results are a list of elements of the form
+ (:query \"query string\"
+ :count <total number matching count>
+ :unread <number of unread messages in count>)"
+ (plist-get mu4e--server-props :queries))
+
+(defun mu4e-last-query-result (query)
+ "Get the last result for some QUERY or nil if not found."
+ (seq-find
+ (lambda (elm) (string= (plist-get elm :query) query))
+ (mu4e-last-query-results)))
+
+\f
+;;; Logging / debugging
+
+(defconst mu4e--log-max-size 1000000
+ "Max number of characters to keep around in the log buffer.")
+(defconst mu4e--log-buffer-name "*mu4e-log*"
+ "Name of the logging buffer.")
+
+(defun mu4e--get-log-buffer ()
+ "Fetch (and maybe create) the log buffer."
+ (unless (get-buffer mu4e--log-buffer-name)
+ (with-current-buffer (get-buffer-create mu4e--log-buffer-name)
+ (view-mode)
+ (when (fboundp 'so-long-mode)
+ (unless (eq major-mode 'so-long-mode)
+ (eval '(so-long-mode))))
+ (setq buffer-undo-list t)))
+ mu4e--log-buffer-name)
+
+(defun mu4e-log (type frm &rest args)
+ "Log a message of TYPE with format-string FRM and ARGS.
+Use the mu4e log buffer for this. If the variable mu4e-debug is
+non-nil. Type is a symbol, either `to-server', `from-server' or
+`misc'.
+
+This function is meant for debugging."
+ (when mu4e-debug
+ (with-current-buffer (mu4e--get-log-buffer)
+ (let* ((inhibit-read-only t)
+ (tstamp (propertize (format-time-string "%Y-%m-%d %T.%3N"
+ (current-time))
+ 'face 'font-lock-string-face))
+ (msg-face
+ (pcase type
+ ('from-server 'font-lock-type-face)
+ ('to-server 'font-lock-function-name-face)
+ ('misc 'font-lock-variable-name-face)
+ ('error 'font-lock-warning-face)
+ (_ (mu4e-error "Unsupported log type"))))
+ (msg (propertize (apply 'format frm args) 'face msg-face)))
+ (save-excursion
+ (goto-char (point-max))
+ (insert tstamp
+ (pcase type
+ ('from-server " <- ")
+ ('to-server " -> ")
+ ('error " !! ")
+ (_ " "))
+ msg "\n")
+ ;; if `mu4e-log-max-lines is specified and exceeded, clearest the
+ ;; oldest lines
+ (when (> (buffer-size) mu4e--log-max-size)
+ (goto-char (- (buffer-size) mu4e--log-max-size))
+ (beginning-of-line)
+ (delete-region (point-min) (point))))))))
+
+(defun mu4e-toggle-logging ()
+ "Toggle `mu4e-debug'.
+In debug-mode, mu4e logs some of its internal workings to a
+log-buffer. See `mu4e-show-log'."
+ (interactive)
+ (mu4e-log 'misc "logging disabled")
+ (setq mu4e-debug (not mu4e-debug))
+ (mu4e-message "debug logging has been %s"
+ (if mu4e-debug "enabled" "disabled"))
+ (mu4e-log 'misc "logging enabled"))
+
+(defun mu4e-show-log ()
+ "Visit the mu4e debug log."
+ (interactive)
+ (unless mu4e-debug (mu4e-toggle-logging))
+ (let ((buf (get-buffer mu4e--log-buffer-name)))
+ (unless (buffer-live-p buf)
+ (mu4e-warn "No debug log available"))
+ (switch-to-buffer buf)))
+
+
+\f
+;;; Flags
+;; Converting flags->string and vice-versa
+
+(defun mu4e-flags-to-string (flags)
+ "Convert a list of Maildir[1] FLAGS into a string.
+
+See `mu4e-string-to-flags'. \[1\]:
+http://cr.yp.to/proto/maildir.html."
+ (seq-sort
+ '<
+ (seq-mapcat
+ (lambda (flag)
+ (pcase flag
+ (`draft "D")
+ (`flagged "F")
+ (`new "N")
+ (`passed "P")
+ (`replied "R")
+ (`seen "S")
+ (`trashed "T")
+ (`attach "a")
+ (`encrypted "x")
+ (`signed "s")
+ (`unread "u")
+ (_ "")))
+ (seq-uniq flags) 'string)))
+
+(defun mu4e-string-to-flags (str)
+ "Convert a STR with Maildir[1] flags into a list of flags.
+
+See `mu4e-string-to-flags'. \[1\]:
+http://cr.yp.to/proto/maildir.html."
+ (seq-uniq
+ (seq-filter
+ 'identity
+ (seq-mapcat
+ (lambda (kar)
+ (list
+ (pcase kar
+ ('?D 'draft)
+ ('?F 'flagged)
+ ('?P 'passed)
+ ('?R 'replied)
+ ('?S 'seen)
+ ('?T 'trashed)
+ (_ nil))))
+ str))))
+
+\f
+;;; Misc
+(defun mu4e-copy-thing-at-point ()
+ "Copy e-mail address or URL at point to the kill ring.
+If there is not e-mail address at point, do nothing."
+ (interactive)
+ (let* ((thing (and (thing-at-point 'email)
+ (string-trim (thing-at-point 'email 'no-props) "<" ">")))
+ (thing (or thing (thing-at-point 'url 'no-props))))
+ (when thing
+ (kill-new thing)
+ (mu4e-message "Copied '%s' to kill-ring" thing))))
+
+(defun mu4e-display-size (size)
+ "Get a human-friendly string representation of SIZE (in bytes)."
+ (cond
+ ((>= size 1000000)
+ (format "%2.1fM" (/ size 1000000.0)))
+ ((and (>= size 1000) (< size 1000000))
+ (format "%2.1fK" (/ size 1000.0)))
+ ((< size 1000)
+ (format "%d" size))
+ (t "?")))
+
+
+(defun mu4e-split-ranges-to-numbers (str n)
+ "Convert STR containing attachment numbers into a list of numbers.
+
+STR is a string; N is the highest possible number in the list.
+This includes expanding e.g. 3-5 into 3,4,5. If the letter
+\"a\" ('all')) is given, that is expanded to a list with numbers
+[1..n]."
+ (let ((str-split (split-string str))
+ beg end list)
+ (dolist (elem str-split list)
+ ;; special number "a" converts into all attachments 1-N.
+ (when (equal elem "a")
+ (setq elem (concat "1-" (int-to-string n))))
+ (if (string-match "\\([0-9]+\\)-\\([0-9]+\\)" elem)
+ ;; we have found a range A-B, which needs converting
+ ;; into the numbers A, A+1, A+2, ... B.
+ (progn
+ (setq beg (string-to-number (match-string 1 elem))
+ end (string-to-number (match-string 2 elem)))
+ (while (<= beg end)
+ (cl-pushnew beg list :test 'equal)
+ (setq beg (1+ beg))))
+ ;; else just a number
+ (cl-pushnew (string-to-number elem) list :test 'equal)))
+ ;; Check that all numbers are valid.
+ (mapc
+ (lambda (x)
+ (cond
+ ((> x n)
+ (mu4e-warn "Attachment %d bigger than maximum (%d)" x n))
+ ((< x 1)
+ (mu4e-warn "Attachment number must be greater than 0 (%d)" x))))
+ list)))
+
+(defun mu4e-make-temp-file (ext)
+ "Create a self-destructing temporary file with extension EXT.
+The file will self-destruct in a short while, enough to open it
+in an external program."
+ (let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext))))
+ (run-at-time "30 sec" nil
+ (lambda () (ignore-errors (delete-file tmpfile))))
+ tmpfile))
+
+(defsubst mu4e-is-mode-or-derived-p (mode)
+ "Is the current mode equal to MODE or derived from it?"
+ (or (eq major-mode mode) (derived-mode-p mode)))
+
+(defun mu4e-display-manual ()
+ "Display the mu4e manual page for the current mode.
+Or go to the top level if there is none."
+ (interactive)
+ (info (pcase major-mode
+ ('mu4e-main-mode "(mu4e)Main view")
+ ('mu4e-headers-mode "(mu4e)Headers view")
+ ('mu4e-view-mode "(mu4e)Message view")
+ (_ "mu4e"))))
+
+\f
+;;; bookmarks
+(defun mu4e--make-bookmark-record ()
+ "Create a bookmark for the message at point."
+ (let* ((msg (mu4e-message-at-point))
+ (subject (or (plist-get msg :subject) "No subject"))
+ (date (plist-get msg :date))
+ (date (if date (format-time-string "%F: " date) ""))
+ (title (format "%s%s" date subject))
+ (msgid (or (plist-get msg :message-id)
+ (mu4e-error "Cannot bookmark message without message-id"))))
+ `(,title
+ ,@(bookmark-make-record-default 'no-file 'no-context)
+ (message-id . ,msgid)
+ (handler . mu4e--jump-to-bookmark))))
+
+(declare-function mu4e-view-message-with-message-id "mu4e-view")
+(declare-function mu4e-message-at-point "mu4e-message")
+
+(defun mu4e--jump-to-bookmark (bookmark)
+ "View the message referred to by BOOKMARK."
+ (when-let ((msgid (bookmark-prop-get bookmark 'message-id)))
+ (mu4e-view-message-with-message-id msgid)))
+
+\f;;; Macros
+
+(defmacro mu4e-setq-if-nil (var val)
+ "Set VAR to VAL if VAR is nil."
+ `(unless ,var (setq ,var ,val)))
+
+(provide 'mu4e-helpers)
+;;; mu4e-helpers.el ends here
--- /dev/null
+;;; mu4e-icalendar.el --- reply to iCalendar meeting requests (part of mu4e) -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2019- Christophe Troestler
+
+;; Author: Christophe Troestler <Christophe.Troestler@umons.ac.be>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: email icalendar
+;; Version: 0.0
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; To install:
+;; (require 'mu4e-icalendar)
+;; (mu4e-icalendar-setup)
+;; Optional
+;; (setq mu4e-icalendar-trash-after-reply t)
+
+;; By default, the original message is not cited. However, if you
+;; would like to reply to it, the citation is in the kill-ring (paste
+;; it with `yank').
+
+;; To add the event to a diary file of your choice:
+;; (setq mu4e-icalendar-diary-file "/path/to/your/diary")
+;; If the file specified is not your main diary file, add
+;; #include "/path/to/your/diary"
+;; to you main diary file to display the events.
+
+;; To enable optional iCalendar->Org sync functionality
+;; NOTE: both the capture file and the headline(s) inside must already exist
+;; (require 'org-agenda)
+;; (setq gnus-icalendar-org-capture-file "~/org/notes.org")
+;; (setq gnus-icalendar-org-capture-headline '("Calendar"))
+;; (gnus-icalendar-org-setup)
+
+;;; Code:
+
+(require 'gnus-icalendar)
+(require 'cl-lib)
+
+(require 'mu4e-mark)
+(require 'mu4e-helpers)
+(require 'mu4e-contacts)
+(require 'mu4e-headers)
+(require 'mu4e-view)
+
+\f
+;;; Configuration
+;;;; Calendar
+
+(defgroup mu4e-icalendar nil
+ "Icalendar related settings."
+ :group 'mu4e)
+
+(defcustom mu4e-icalendar-trash-after-reply nil
+ "If non-nil, trash the icalendar invitation after replying."
+ :type 'boolean
+ :group 'mu4e-icalendar)
+
+(defcustom mu4e-icalendar-diary-file nil
+ "If non-nil, the file in which to add events upon reply."
+ :type '(choice (const :tag "Do not insert a diary entry" nil)
+ (string :tag "Insert a diary entry in this file"))
+ :group 'mu4e-icalendar)
+
+\f
+;;;###autoload
+(defun mu4e-icalendar-setup ()
+ "Perform the necessary initialization to use mu4e-icalendar."
+ (gnus-icalendar-setup)
+ (cl-defmethod gnus-icalendar-event:inline-reply-buttons :around
+ ((event gnus-icalendar-event) handle)
+ (if (and (boundp 'mu4e~view-rendering)
+ (gnus-icalendar-event:rsvp event))
+ (let ((method (gnus-icalendar-event:method event)))
+ (when (or (string= method "REQUEST") (string= method "PUBLISH"))
+ `(("Accept" mu4e-icalendar-reply (,handle accepted ,event))
+ ("Tentative" mu4e-icalendar-reply (,handle tentative ,event))
+ ("Decline" mu4e-icalendar-reply (,handle declined ,event)))))
+ (cl-call-next-method event handle))))
+
+(defun mu4e~icalendar-has-email (email list)
+ "Check that EMAIL is in LIST."
+ (let ((email (downcase email)))
+ (cl-find-if (lambda (c) (let ((e (mu4e-contact-email c)))
+ (and (stringp e) (string= email (downcase e)))))
+ list)))
+
+(defun mu4e-icalendar-reply (data)
+ "Reply to the text/calendar event present in DATA."
+ ;; Based on `gnus-icalendar-reply'.
+ (let* ((handle (car data))
+ (status (cadr data))
+ (event (caddr data))
+ (gnus-icalendar-additional-identities (mu4e-personal-addresses 'no-regexp))
+ (reply (gnus-icalendar-with-decoded-handle
+ handle
+ (gnus-icalendar-event-reply-from-buffer
+ (current-buffer) status (gnus-icalendar-identities))))
+ (msg (mu4e-message-at-point 'noerror))
+ (charset (cdr (assoc 'charset (mm-handle-type handle)))))
+ (when reply
+ (cl-labels
+ ((fold-icalendar-buffer
+ ()
+ (goto-char (point-min))
+ (while (re-search-forward "^\\(.\\{72\\}\\)\\(.+\\)$" nil t)
+ (replace-match "\\1\n \\2")
+ (goto-char (line-beginning-position)))))
+
+ (let ((ical-name gnus-icalendar-reply-bufname))
+ (with-current-buffer (get-buffer-create ical-name)
+ (delete-region (point-min) (point-max))
+ (insert reply)
+ (fold-icalendar-buffer)
+ (when (and charset (string= (downcase charset) "utf-8"))
+ (decode-coding-region (point-min) (point-max) 'utf-8)))
+ ;; Compose the reply message.
+ (save-excursion
+ (let ((message-signature nil)
+ (mu4e-compose-cite-function #'mu4e~icalendar-delete-citation)
+ (mu4e-sent-messages-behavior 'delete)
+ (mu4e-compose-reply-recipients 'sender)
+ (ical-msg (cl-copy-list msg)))
+ ;; Make sure the reply is sent to email of the organiser with proper name.
+ (let* ((organizer (gnus-icalendar-event:organizer event))
+ (reply-to (car (plist-get msg :reply-to)))
+ (from (car (plist-get msg :from)))
+ (name (or (plist-get reply-to :name)
+ (plist-get from :name))))
+ ;; Add :reply-to field when incomplete or absent
+ (unless (or (string= organizer "")
+ (mu4e~icalendar-has-email organizer reply-to))
+ (plist-put ical-msg :reply-to `((:name ,name :email ,organizer))))
+ (plist-put ical-msg :subject
+ (concat (capitalize (symbol-name status))
+ ": " (gnus-icalendar-event:summary event))))
+ (mu4e~compose-handler
+ 'reply ical-msg
+ `((:buffer-name ,ical-name
+ :mime-type "text/calendar; method=REPLY; charset=utf-8")))
+ (message-goto-body)
+ (set-buffer-modified-p nil); not yet modified by user
+ (when mu4e-icalendar-trash-after-reply
+ ;; Override `mu4e-sent-handler' set by `mu4e-compose-mode' to
+ ;; also trash the message (thus must be appended to hooks).
+ (add-hook 'message-sent-hook
+ (mu4e~icalendar-trash-message-hook msg)
+ 90 t)))))
+
+ ;; Back in article buffer
+ (setq-local gnus-icalendar-reply-status status)
+
+ (when gnus-icalendar-org-enabled-p
+ (if (gnus-icalendar-find-org-event-file event)
+ (gnus-icalendar--update-org-event event status)
+ (gnus-icalendar:org-event-save event status)))
+ (when mu4e-icalendar-diary-file
+ (mu4e~icalendar-insert-diary event status
+ mu4e-icalendar-diary-file))))))
+
+(defun mu4e~icalendar-delete-citation ()
+ "Function passed to `mu4e-compose-cite-function' to remove the citation."
+ (message-cite-original-without-signature)
+ (kill-region (point-min) (point-max)))
+
+(defun mu4e~icalendar-trash-message (original-msg)
+ "Trash the message ORIGINAL-MSG and move to the next one."
+ (lambda (docid path)
+ "See `mu4e-sent-handler' for DOCID and PATH."
+ (mu4e-sent-handler docid path)
+ (let* ((docid (mu4e-message-field original-msg :docid))
+ (markdescr (assq 'trash mu4e-marks))
+ (action (plist-get (cdr markdescr) :action))
+ (target (mu4e-get-trash-folder original-msg)))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (run-hook-with-args 'mu4e-mark-execute-pre-hook 'trash original-msg)
+ (funcall action docid original-msg target))
+ (when (and (mu4e~headers-view-this-message-p docid)
+ (buffer-live-p (mu4e-get-view-buffer)))
+ (switch-to-buffer (mu4e-get-view-buffer))
+ (or (mu4e-view-headers-next)
+ (kill-buffer-and-window))))))
+
+(defun mu4e~icalendar-trash-message-hook (original-msg)
+ (lambda () (setq mu4e-sent-func
+ (mu4e~icalendar-trash-message original-msg))))
+
+(defun mu4e~icalendar-insert-diary (event reply-status filename)
+ "Insert a diary entry for the EVENT in file named FILENAME.
+REPLY-STATUS is the status of the reply. The possible values are
+given in the doc of `gnus-icalendar-event-reply-from-buffer'."
+ ;; FIXME: handle recurring events
+ (let* ((beg (gnus-icalendar-event:start-time event))
+ (beg-date (format-time-string "%d/%m/%Y" beg))
+ (beg-time (format-time-string "%H:%M" beg))
+ (end (gnus-icalendar-event:end-time event))
+ (end-date (format-time-string "%d/%m/%Y" end))
+ (end-time (format-time-string "%H:%M" end))
+ (summary (gnus-icalendar-event:summary event))
+ (location (gnus-icalendar-event:location event))
+ (status (capitalize (symbol-name reply-status)))
+ (txt (if location
+ (format "%s (%s)\n %s " summary status location)
+ (format "%s (%s)" summary status))))
+ (with-temp-buffer
+ (if (string= beg-date end-date)
+ (insert beg-date " " beg-time "-" end-time " " txt "\n")
+ (insert beg-date " " beg-time " Start of: " txt "\n")
+ (insert beg-date " " end-time " End of: " txt "\n"))
+ (write-region (point-min) (point-max) filename t))))
+
+;;; _
+(provide 'mu4e-icalendar)
+;;; mu4e-icalendar.el ends here
--- /dev/null
+;;; mu4e-lists.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file, we create a table of list-id -> shortname for mailing lists.
+;; The shortname (friendly) should a at most 8 characters, camel-case
+
+;;; Code:
+\f
+;;; Configuration
+(defvar mu4e-mailing-lists
+ '( ("bbdb-info.lists.sourceforge.net" . "BBDB")
+ ("boost-announce.lists.boost.org" . "BoostA")
+ ("boost-interest.lists.boost.org" . "BoostI")
+ ("conkeror.mozdev.org" . "Conkeror")
+ ("curl-library.cool.haxx.se" . "LibCurl")
+ ("crypto-gram-list.schneier.com " . "CryptoGr")
+ ("dbus.lists.freedesktop.org" . "DBus")
+ ("desktop-devel-list.gnome.org" . "GnomeDT")
+ ("discuss-webrtc.googlegroups.com" . "WebRTC")
+ ("emacs-devel.gnu.org" . "EmacsDev")
+ ("emacs-orgmode.gnu.org" . "Orgmode")
+ ("emms-help.gnu.org" . "Emms")
+ ("enlightenment-devel.lists.sourceforge.net" . "E-Dev")
+ ("erlang-questions.erlang.org" . "Erlang")
+ ("evolution-hackers.lists.ximian.com" . "EvoDev")
+ ("farsight-devel.lists.sourceforge.net" . "Farsight")
+ ("mailman.lists.freedesktop.org" . "FDeskTop")
+ ("gcc-help.gcc.gnu.org" . "Gcc")
+ ("gmime-devel-list.gnome.org" . "GMimeDev")
+ ("gnome-shell-list.gnome.org" . "GnomeSh")
+ ("gnu-emacs-sources.gnu.org" . "EmacsSrc")
+ ("gnupg-users.gnupg.org" . "GnupgU")
+ ("gpe.handhelds.org" . "GPE")
+ ("gstreamer-devel.lists.freedesktop.org" . "GstDev")
+ ("gstreamer-devel.lists.sourceforge.net" . "GstDev")
+ ("gstreamer-openmax.lists.sourceforge.net" . "GstOmx")
+ ("gtk-devel-list.gnome.org" . "GtkDev")
+ ("gtkmm-list.gnome.org" . "GtkmmDev")
+ ("guile-devel.gnu.org" . "GuileDev")
+ ("guile-user.gnu.org" . "GuileUsr")
+ ("help-gnu-emacs.gnu.org" . "EmacsUsr")
+ ("lggdh-algemeen.vvtp.tudelft.nl" . "LGGDH")
+ ("linux-bluetooth.vger.kernel.org" . "Bluez")
+ ("maemo-developers.maemo.org" . "MaemoDev")
+ ("maemo-users.maemo.org" . "MaemoUsr")
+ ("monit-general.nongnu.org" . "Monit")
+ ("mu-discuss.googlegroups.com" . "Mu")
+ ("nautilus-list.gnome.org" . "Nautilus")
+ ("notmuch.notmuchmail.org" . "Notmuch")
+ ("orbit-list.gnome.org" . "ORBit")
+ ("pulseaudio-discuss.lists.freedesktop.org" . "PulseA")
+ ("sqlite-announce.sqlite.org" . "SQliteAnn")
+ ("sqlite-dev.sqlite.org" . "SQLiteDev")
+ ("sup-talk.rubyforge.org" . "Sup")
+ ("sylpheed-claws-users.lists.sourceforge.net" . "Sylpheed")
+ ("tinymail-devel-list.gnome.org" . "Tinymail")
+ ("unicode.sarasvati.unicode.org" . "Unicode")
+ ("xapian-discuss.lists.xapian.org" . "Xapian")
+ ("xdg.lists.freedesktop.org" . "XDG")
+ ("wl-en.lists.airs.net" . "Wdrlust")
+ ("wl-en.ml.gentei.org" . "WdrLust")
+ ("xapian-devel.lists.xapian.org" . "Xapian")
+ ("zsh-users.zsh.org" . "ZshUsr"))
+ "AList of cells (MAILING-LIST-ID . SHORTNAME).")
+
+(defcustom mu4e-user-mailing-lists nil
+ "An alist with cells (MAILING-LIST-ID . SHORTNAME).
+These are used in addition to the built-in list `mu4e-mailing-lists'."
+ :group 'mu4e-headers
+ :type '(repeat (cons string string)))
+
+(defcustom mu4e-mailing-list-patterns nil
+ "A list of regexps to capture a shortname out of a list-id.
+For the first regex that matches, its first matchgroup will be
+used as the shortname."
+ :group 'mu4e-headers
+ :type '(repeat (regexp)))
+\f
+
+(defvar mu4e--lists-hash nil
+ "Hashtable of mailing-list-id => shortname.
+Based on `mu4e-mailing-lists' and `mu4e-user-mailing-lists'.")
+
+\f
+(defun mu4e-get-mailing-list-shortname (list-id)
+ "Get the shortname for a mailing-list with list-id LIST-ID.
+Based on `mu4e-mailing-lists', `mu4e-user-mailing-lists', and
+`mu4e-mailing-list-patterns'."
+ (unless mu4e--lists-hash
+ (setq mu4e--lists-hash (make-hash-table :test 'equal))
+ (dolist (cell mu4e-mailing-lists)
+ (puthash (car cell) (cdr cell) mu4e--lists-hash))
+ (dolist (cell mu4e-user-mailing-lists)
+ (puthash (car cell) (cdr cell) mu4e--lists-hash)))
+ (or
+ (gethash list-id mu4e--lists-hash)
+ (and (boundp 'mu4e-mailing-list-patterns)
+ (seq-drop-while
+ (lambda (pattern)
+ (not (string-match pattern list-id)))
+ mu4e-mailing-list-patterns)
+ (match-string 1 list-id))
+ ;; if it's not in the db, take the part until the first dot if there is one;
+ ;; otherwise just return the whole thing
+ (if (string-match "\\([^.]*\\)\\." list-id)
+ (match-string 1 list-id)
+ list-id)))
+;;; _
+(provide 'mu4e-lists)
+;;; mu4e-lists.el ends here
--- /dev/null
+;;; mu4e-main.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'smtpmail) ;; the queueing stuff (silence elint)
+(require 'mu4e-helpers) ;; utility functions
+(require 'mu4e-context) ;; the context
+(require 'mu4e-bookmarks)
+(require 'mu4e-folders)
+(require 'mu4e-update)
+(require 'mu4e-contacts)
+(require 'mu4e-search)
+(require 'mu4e-vars) ;; mu-wide variables
+
+(declare-function mu4e-compose-new "mu4e-compose")
+(declare-function mu4e~headers-jump-to-maildir "mu4e-headers")
+(declare-function mu4e-quit "mu4e")
+
+(require 'cl-lib)
+
+\f
+;; Configuration
+
+(define-obsolete-variable-alias
+ 'mu4e-main-buffer-hide-personal-addresses
+ 'mu4e-main-hide-personal-addresses "1.5.7")
+
+(defvar mu4e-main-hide-personal-addresses nil
+ "Whether to hide the personal address in the main view.
+
+This can be useful to avoid the noise when there are many, and
+also hides the warning if your `user-mail-address' is not part of
+the personal addresses.")
+
+(defvar mu4e-main-hide-fully-read nil
+ "Whether to hide bookmarks or maildirs without unread messages.")
+
+\f
+;;; Mode
+(define-derived-mode mu4e-org-mode org-mode "mu4e:org"
+ "Major mode for mu4e documents.")
+
+(defun mu4e-info (path)
+ "Show a buffer with the information (an org-file) at PATH."
+ (unless (file-exists-p path)
+ (mu4e-error "Cannot find %s" path))
+ (let ((curbuf (current-buffer)))
+ (find-file path)
+ (mu4e-org-mode)
+ (setq buffer-read-only t)
+ (define-key mu4e-org-mode-map (kbd "q")
+ `(lambda ()
+ (interactive)
+ (bury-buffer)
+ (switch-to-buffer ,curbuf)))))
+
+(defun mu4e-about ()
+ "Show the mu4e \"About\" page."
+ (interactive)
+ (mu4e-info (concat mu4e-doc-dir "/mu4e-about.org")))
+
+(defun mu4e-news ()
+ "Show page with news for the current version of mu4e."
+ (interactive)
+ (mu4e-info (concat mu4e-doc-dir "/NEWS.org")))
+
+
+(defvar mu4e-main-mode-map
+ (let ((map (make-sparse-keymap)))
+
+ (define-key map "q" #'mu4e-quit)
+ (define-key map "j" #'mu4e~headers-jump-to-maildir)
+ (define-key map "C" #'mu4e-compose-new)
+
+ (define-key map "m" #'mu4e--main-toggle-mail-sending-mode)
+ (define-key map "f" #'smtpmail-send-queued-mail)
+ ;;
+ (define-key map "U" #'mu4e-update-mail-and-index)
+ (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index)
+ ;; for terminal users
+ (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+
+ (define-key map "S" #'mu4e-kill-update-mail)
+ (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index)
+ (define-key map ";"
+ (lambda()(interactive)(mu4e-context-switch)(revert-buffer)))
+
+ (define-key map "$" #'mu4e-show-log)
+ (define-key map "A" #'mu4e-about)
+ (define-key map "N" #'mu4e-news)
+ (define-key map "H" #'mu4e-display-manual)
+ map)
+ "Keymap for the *mu4e-main* buffer.")
+
+(define-derived-mode mu4e-main-mode special-mode "mu4e:main"
+ "Major mode for the mu4e main screen.
+\\{mu4e-main-mode-map}."
+ (setq truncate-lines t
+ overwrite-mode 'overwrite-mode-binary)
+ (mu4e-context-minor-mode)
+ (mu4e-search-minor-mode)
+ (mu4e-update-minor-mode)
+ (set (make-local-variable 'revert-buffer-function) #'mu4e--main-view-real))
+
+
+(defun mu4e--main-action-str (str &optional func-or-shortcut)
+ "Highlight the first occurrence of [.] in STR.
+If FUNC-OR-SHORTCUT is non-nil and if it is a function, call it
+when STR is clicked (using RET or mouse-2); if FUNC-OR-SHORTCUT is
+a string, execute the corresponding keyboard action when it is
+clicked."
+ (let ((newstr
+ (replace-regexp-in-string
+ "\\[\\(..?\\)\\]"
+ (lambda(m)
+ (format "[%s]"
+ (propertize (match-string 1 m) 'face 'mu4e-highlight-face)))
+ str))
+ (map (make-sparse-keymap))
+ (func (if (functionp func-or-shortcut)
+ func-or-shortcut
+ (if (stringp func-or-shortcut)
+ (lambda()(interactive)
+ (execute-kbd-macro func-or-shortcut))))))
+ (define-key map [mouse-2] func)
+ (define-key map (kbd "RET") func)
+ (put-text-property 0 (length newstr) 'keymap map newstr)
+ (put-text-property (string-match "\\[.+$" newstr)
+ ;; only subtract one from length of newstr if we're
+ ;; actually consuming the first letter (e.g.
+ ;; `func-or-shortcut' is a function, meaning we put
+ ;; braces around the first letter of `str')
+ (if (stringp func-or-shortcut)
+ (length newstr)
+ (- (length newstr) 1))
+ 'mouse-face 'highlight newstr)
+ newstr))
+
+
+
+(defun mu4e--longest-of-maildirs-and-bookmarks ()
+ "Return the length of longest name of bookmarks and maildirs."
+ (cl-loop for b in (append (mu4e-bookmarks)
+ (mu4e--maildirs-with-query))
+ maximize (string-width (plist-get b :name))))
+
+(defun mu4e--main-bookmarks ()
+ "Return the entries for the bookmarks menu."
+ ;; TODO: it's a bit uncool to hard-code the "b" shortcut...
+ (cl-loop with bmks = (mu4e-bookmarks)
+ with longest = (mu4e--longest-of-maildirs-and-bookmarks)
+ with queries = (mu4e-last-query-results)
+ for bm in bmks
+ for key = (string (plist-get bm :key))
+ for name = (plist-get bm :name)
+ for query = (funcall (or mu4e-query-rewrite-function #'identity)
+ (plist-get bm :query))
+ for qcounts = (and (stringp query)
+ (cl-loop for q in queries
+ when (string=
+ (decode-coding-string
+ (plist-get q :query) 'utf-8 t)
+ query)
+ collect q))
+ for unread = (and qcounts (plist-get (car qcounts) :unread))
+ when (not (plist-get bm :hide))
+ when (not (and mu4e-main-hide-fully-read (eq unread 0)))
+ concat (concat
+ ;; menu entry
+ (mu4e--main-action-str
+ (concat "\t* [b" key "] " name)
+ (concat "b" key))
+ ;; append all/unread numbers, if available.
+ (if qcounts
+ (let ((unread (plist-get (car qcounts) :unread))
+ (count (plist-get (car qcounts) :count)))
+ (format
+ "%s (%s/%s)"
+ (make-string (- longest (string-width name)) ? )
+ (propertize (number-to-string unread)
+ 'face 'mu4e-header-key-face)
+ count))
+ "")
+ "\n")))
+
+
+(defun mu4e--main-maildirs ()
+ "Return a string of maildirs with their counts."
+ (cl-loop with mds = (mu4e--maildirs-with-query)
+ with longest = (mu4e--longest-of-maildirs-and-bookmarks)
+ with queries = (plist-get mu4e--server-props :queries)
+ for m in mds
+ for key = (string (plist-get m :key))
+ for name = (plist-get m :name)
+ for query = (plist-get m :query)
+ for qcounts = (and (stringp query)
+ (cl-loop for q in queries
+ when (string=
+ (decode-coding-string
+ (plist-get q :query)
+ 'utf-8 t)
+ query)
+ collect q))
+ for unread = (and qcounts (plist-get (car qcounts) :unread))
+ when (not (plist-get m :hide))
+ when (not (and mu4e-main-hide-fully-read (eq unread 0)))
+ concat (concat
+ ;; menu entry
+ (mu4e--main-action-str
+ (concat "\t* [j" key "] " name)
+ (concat "j" key))
+ ;; append all/unread numbers, if available.
+ (if qcounts
+ (let ((unread (plist-get (car qcounts) :unread))
+ (count (plist-get (car qcounts) :count)))
+ (format
+ "%s (%s/%s)"
+ (make-string (- longest (string-width name)) ? )
+ (propertize (number-to-string unread)
+ 'face 'mu4e-header-key-face)
+ count))
+ "")
+ "\n")))
+
+
+(defun mu4e--key-val (key val &optional unit)
+ "Show a KEY / VAL pair, with optional UNIT."
+ (concat
+ "\t* "
+ (propertize (format "%-20s" key) 'face 'mu4e-header-title-face)
+ ": "
+ (propertize val 'face 'mu4e-header-key-face)
+ (if unit
+ (propertize (concat " " unit) 'face 'mu4e-header-title-face)
+ "")
+ "\n"))
+
+;; NEW This is the old `mu4e--main-view' function but without
+;; buffer switching at the end.
+(defun mu4e--main-view-real (_ignore-auto _noconfirm)
+ "The revert buffer function for `mu4e-main-mode'."
+ (mu4e--main-view-real-1 'refresh))
+
+(declare-function mu4e--start "mu4e")
+
+(defun mu4e--main-view-real-1 (&optional refresh)
+ "Create `mu4e-main-buffer-name' and set it up.
+When REFRESH is non nil refresh infos from server."
+ (let ((inhibit-read-only t))
+ ;; Maybe refresh infos from server.
+ (if refresh
+ (mu4e--start 'mu4e--main-redraw-buffer)
+ (mu4e--main-redraw-buffer))))
+
+(defun mu4e--main-redraw-buffer ()
+ "Redraw the main buffer."
+ (with-current-buffer mu4e-main-buffer-name
+ (let ((inhibit-read-only t)
+ (pos (point))
+ (addrs (mu4e-personal-addresses)))
+ (erase-buffer)
+ (insert
+ "* "
+ (propertize "mu4e" 'face 'mu4e-header-key-face)
+ (propertize " - mu for emacs version " 'face 'mu4e-title-face)
+ (propertize mu4e-mu-version 'face 'mu4e-header-key-face)
+ "\n\n"
+ (propertize " Basics\n\n" 'face 'mu4e-title-face)
+ (mu4e--main-action-str
+ "\t* [j]ump to some maildir\n" #'mu4e~headers-jump-to-maildir)
+ (mu4e--main-action-str
+ "\t* enter a [s]earch query\n" #'mu4e-search)
+ (mu4e--main-action-str
+ "\t* [C]ompose a new message\n" #'mu4e-compose-new)
+ "\n"
+ (propertize " Bookmarks\n\n" 'face 'mu4e-title-face)
+ (mu4e--main-bookmarks)
+ "\n"
+ (propertize " Maildirs\n\n" 'face 'mu4e-title-face)
+ (mu4e--main-maildirs)
+ "\n"
+ (propertize " Misc\n\n" 'face 'mu4e-title-face)
+
+ (mu4e--main-action-str "\t* [;]Switch context\n"
+ (lambda()(interactive)
+ (mu4e-context-switch)(revert-buffer)))
+
+ (mu4e--main-action-str "\t* [U]pdate email & database\n"
+ 'mu4e-update-mail-and-index)
+
+ ;; show the queue functions if `smtpmail-queue-dir' is defined
+ (if (file-directory-p smtpmail-queue-dir)
+ (mu4e--main-view-queue)
+ "")
+ "\n"
+ (mu4e--main-action-str "\t* [N]ews\n" #'mu4e-news)
+ (mu4e--main-action-str "\t* [A]bout mu4e\n" #'mu4e-about)
+ (mu4e--main-action-str "\t* [H]elp\n" #'mu4e-display-manual)
+ (mu4e--main-action-str "\t* [q]uit\n" #'mu4e-quit)
+
+ "\n"
+ (propertize " Info\n\n" 'face 'mu4e-title-face)
+ (mu4e--key-val "last updated" (current-time-string (plist-get mu4e-index-update-status :tstamp)))
+ (mu4e--key-val "database-path" (mu4e-database-path))
+ (mu4e--key-val "maildir" (mu4e-root-maildir))
+ (mu4e--key-val "in store"
+ (format "%d" (plist-get mu4e--server-props :doccount))
+ "messages")
+ (if mu4e-main-hide-personal-addresses ""
+ (mu4e--key-val "personal addresses"
+ (if addrs (mapconcat #'identity addrs ", " ) "none"))))
+
+ (if mu4e-main-hide-personal-addresses ""
+ (unless (mu4e-personal-address-p user-mail-address)
+ (mu4e-message (concat
+ "Tip: `user-mail-address' ('%s') is not part "
+ "of mu's addresses; add it with 'mu init
+ --my-address='") user-mail-address)))
+ (mu4e-main-mode)
+ (goto-char pos))))
+
+(defun mu4e--main-view-queue ()
+ "Display queue-related actions in the main view."
+ (concat
+ (mu4e--main-action-str "\t* toggle [m]ail sending mode "
+ 'mu4e--main-toggle-mail-sending-mode)
+ "(currently "
+ (propertize (if smtpmail-queue-mail "queued" "direct")
+ 'face 'mu4e-header-key-face)
+ ")\n"
+ (let ((queue-size (mu4e--main-queue-size)))
+ (if (zerop queue-size)
+ ""
+ (mu4e--main-action-str
+ (format "\t* [f]lush %s queued %s\n"
+ (propertize (int-to-string queue-size)
+ 'face 'mu4e-header-key-face)
+ (if (> queue-size 1) "mails" "mail"))
+ 'smtpmail-send-queued-mail)))))
+
+(defun mu4e--main-queue-size ()
+ "Return, as an int, the number of emails in the queue."
+ (condition-case nil
+ (with-temp-buffer
+ (insert-file-contents (expand-file-name smtpmail-queue-index-file
+ smtpmail-queue-dir))
+ (count-lines (point-min) (point-max)))
+ (error 0)))
+
+(defun mu4e--main-view (&optional refresh)
+ "Create the mu4e main-view, and switch to it.
+
+When REFRESH is non nil refresh infos from server."
+ (let ((buf (get-buffer-create mu4e-main-buffer-name)))
+ (if (eq mu4e-split-view 'single-window)
+ (if (buffer-live-p (mu4e-get-headers-buffer))
+ (switch-to-buffer (mu4e-get-headers-buffer))
+ (mu4e--main-menu))
+ ;; `mu4e--main-view' is called from `mu4e--start', so don't call it
+ ;; a second time here i.e. do not refresh unless specified
+ ;; explicitly with REFRESH arg.
+ (switch-to-buffer buf)
+ (with-current-buffer buf
+ (mu4e--main-view-real-1 refresh))
+ (goto-char (point-min)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Interactive functions
+;; NEW
+;; Toggle mail sending mode without switching
+(defun mu4e--main-toggle-mail-sending-mode ()
+ "Toggle sending mail mode, either queued or direct."
+ (interactive)
+ (unless (file-directory-p smtpmail-queue-dir)
+ (mu4e-error "`smtpmail-queue-dir' does not exist"))
+ (setq smtpmail-queue-mail (not smtpmail-queue-mail))
+ (message (concat "Outgoing mail will now be "
+ (if smtpmail-queue-mail "queued" "sent directly")))
+ (unless (or (eq mu4e-split-view 'single-window)
+ (not (buffer-live-p (get-buffer mu4e-main-buffer-name))))
+ (with-current-buffer mu4e-main-buffer-name
+ (revert-buffer))))
+
+(defun mu4e--main-menu ()
+ "The mu4e main menu in the mini-buffer."
+ (let ((func (mu4e-read-option
+ "Do: "
+ '(("jump" . mu4e~headers-jump-to-maildir)
+ ("search" . mu4e-search)
+ ("Compose" . mu4e-compose-new)
+ ("bookmarks" . mu4e-headers-search-bookmark)
+ (";Switch context" . mu4e-context-switch)
+ ("Update" . mu4e-update-mail-and-index)
+ ("News" . mu4e-news)
+ ("About" . mu4e-about)
+ ("Help " . mu4e-display-manual)))))
+ (call-interactively func)
+ (when (eq func 'mu4e-context-switch)
+ (sit-for 1)
+ (mu4e--main-menu))))
+
+(provide 'mu4e-main)
+;;; mu4e-main.el ends here
--- /dev/null
+;;; mu4e-mark.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file are function related to marking messages; they assume we are
+;; currently in the headers buffer.
+
+;;; Code:
+
+(require 'mu4e-server)
+(require 'mu4e-message)
+(require 'mu4e-folders)
+
+;; keep byte-compiler happy
+(declare-function mu4e~headers-mark "mu4e-headers")
+(declare-function mu4e~headers-goto-docid "mu4e-headers")
+(declare-function mu4e-headers-next "mu4e-headers")
+
+;;; Variables & constants
+
+(defcustom mu4e-headers-leave-behavior 'ask
+ "What to do when user leaves the headers view.
+That is when he e.g. quits, refreshes or does a new search.
+Value is one of the following symbols:
+- `ask' ask user whether to ignore the marks
+- `apply' automatically apply the marks before doing anything else
+- `ignore' automatically ignore the marks without asking"
+ :type '(choice (const ask :tag "ask user whether to ignore marks")
+ (const apply :tag "apply marks without asking")
+ (const ignore :tag "ignore marks without asking"))
+ :group 'mu4e-headers)
+
+(defcustom mu4e-mark-execute-pre-hook nil
+ "Hook run just *before* a mark is applied to a message.
+The hook function is called with two arguments, the mark being
+executed and the message itself."
+ :type 'hook
+ :group 'mu4e-headers)
+
+(defvar mu4e-headers-show-target t
+ "Whether to show targets (such as \"-> delete\", \"-> /archive\")
+when marking message. Normally, this is useful information for
+the user, however, when you often mark large numbers (thousands)
+of message, showing the target makes this quite a bit
+slower (showing the target uses Emacs overlays, which can be slow
+when overused).")
+
+;;; Insert stuff
+
+(defvar mu4e--mark-map nil
+ "Contains a mapping of docid->markinfo.
+When a message is marked, the information is added here. markinfo
+is a cons cell consisting of the following: (mark . target) where
+MARK is the type of mark (move, trash, delete) TARGET (optional)
+is the target directory (for \"move\")")
+
+;; the mark-map is specific for the current header buffer
+;; currently, there can't be more than one, but we never know what will
+;; happen in the future
+
+;; the fringe is the space on the left of headers, where we put marks below some
+;; handy definitions; only `mu4e-mark-fringe-len' should be change (if ever),
+;; the others follow from that.
+(defconst mu4e--mark-fringe-len 2
+ "Width of the fringe for marks on the left.")
+(defconst mu4e--mark-fringe (make-string mu4e--mark-fringe-len ?\s)
+ "The space on the left of message headers to put marks.")
+(defconst mu4e--mark-fringe-format (format "%%-%ds" mu4e--mark-fringe-len)
+ "Format string to set a mark and leave remaining space.")
+
+(defun mu4e--mark-initialize ()
+ "Initialize the marks-subsystem."
+ (set (make-local-variable 'mu4e--mark-map) (make-hash-table)))
+
+(defun mu4e--mark-clear ()
+ "Clear the marks-subsystem."
+ (clrhash mu4e--mark-map))
+
+(defun mu4e--mark-find-headers-buffer ()
+ "Find the headers buffer, if any."
+ (seq-find (lambda (b)
+ (with-current-buffer b
+ (eq major-mode 'mu4e-headers-mode)))
+ (buffer-list)))
+
+(defmacro mu4e--mark-in-context (&rest body)
+ "Evaluate BODY in the context of the headers buffer.
+The current buffer must be either a headers or view buffer."
+ `(cond
+ ((eq major-mode 'mu4e-headers-mode) ,@body)
+ ((eq major-mode 'mu4e-view-mode)
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (let* ((msg (mu4e-message-at-point))
+ (docid (mu4e-message-field msg :docid)))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (if (mu4e~headers-goto-docid docid)
+ ,@body
+ (mu4e-error "Cannot find message in headers buffer"))))))
+ (t
+ ;; even in other modes (e.g. mu4e-main-mode we try to find
+ ;; the headers buffer
+ (let ((hbuf (mu4e--mark-find-headers-buffer)))
+ (if (buffer-live-p hbuf)
+ (with-current-buffer hbuf ,@body)
+ ,@body)))))
+
+(defconst mu4e-marks
+ '((refile
+ :char ("r" . "▶")
+ :prompt "refile"
+ :dyn-target (lambda (target msg) (mu4e-get-refile-folder msg))
+ :action (lambda (docid msg target)
+ (mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
+ (delete
+ :char ("D" . "x")
+ :prompt "Delete"
+ :show-target (lambda (target) "delete")
+ :action (lambda (docid msg target) (mu4e--server-remove docid)))
+ (flag
+ :char ("+" . "✚")
+ :prompt "+flag"
+ :show-target (lambda (target) "flag")
+ :action (lambda (docid msg target)
+ (mu4e--server-move docid nil "+F-u-N")))
+ (move
+ :char ("m" . "▷")
+ :prompt "move"
+ :ask-target mu4e--mark-get-move-target
+ :action (lambda (docid msg target)
+ (mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
+ (read
+ :char ("!" . "◼")
+ :prompt "!read"
+ :show-target (lambda (target) "read")
+ :action (lambda (docid msg target) (mu4e--server-move docid nil "+S-u-N")))
+ (trash
+ :char ("d" . "▼")
+ :prompt "dtrash"
+ :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg))
+ :action (lambda (docid msg target)
+ (mu4e--server-move docid
+ (mu4e--mark-check-target target) "+T-N")))
+ (unflag
+ :char ("-" . "➖")
+ :prompt "-unflag"
+ :show-target (lambda (target) "unflag")
+ :action (lambda (docid msg target) (mu4e--server-move docid nil "-F-N")))
+ (untrash
+ :char ("=" . "▲")
+ :prompt "=untrash"
+ :show-target (lambda (target) "untrash")
+ :action (lambda (docid msg target) (mu4e--server-move docid nil "-T")))
+ (unread
+ :char ("?" . "◻")
+ :prompt "?unread"
+ :show-target (lambda (target) "unread")
+ :action (lambda (docid msg target) (mu4e--server-move docid nil "-S+u-N")))
+ (unmark
+ :char " "
+ :prompt "unmark"
+ :action (mu4e-error "No action for unmarking"))
+ (action
+ :char ( "a" . "◯")
+ :prompt "action"
+ :ask-target (lambda () (mu4e-read-option "Action: " mu4e-headers-actions))
+ :action (lambda (docid msg actionfunc)
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-headers-action actionfunc)))))
+ (something
+ :char ("*" . "✱")
+ :prompt "*something"
+ :action (mu4e-error "No action for deferred mark")))
+
+ "The list of all the possible marks.
+This is an alist mapping mark symbols to their properties. The
+properties are:
+ :char (string) or (basic . fancy) The character to display in
+ the headers view. Either a single-character string, or a
+ dotted-pair cons cell where the second item will be used if
+ `mu4e-use-fancy-chars' is t, otherwise we'll use
+ the first one. It can also be a plain string for backwards
+ compatibility since we didn't always support
+ `mu4e-use-fancy-chars' here.
+ :prompt (string) The prompt to use when asking for marks (used for
+ example when marking a whole thread)
+ :ask-target (function returning a string) Get the target. This
+ function run once per bulk-operation, and thus is suitable
+ for user-interaction. If nil, the target is nil.
+ :dyn-target (function from (TARGET MSG) to string). Compute
+ the dynamic target. This is run once per message, which is
+ passed as MSG. The default is to just return the target.
+ :show-target (function from TARGET to string) How to display
+ the target.
+ :action (function taking (DOCID MSG TARGET)). The action to
+ apply on the message.")
+
+(defun mu4e-mark-at-point (mark target)
+ "Mark (or unmark) message at point.
+MARK specifies the mark-type. For `move'-marks and `trash'-marks
+the TARGET argument is non-nil and specifies to which maildir the
+message is to be moved/trashed. The function works in both
+headers buffers and message buffers.
+
+The following marks are available, and the corresponding props:
+
+ MARK TARGET description
+ ----------------------------------------------------------
+ `refile' y mark this message for archiving
+ `something' n mark this message for *something* (decided later)
+ `delete' n remove the message
+ `flag' n mark this message for flagging
+ `move' y move the message to some folder
+ `read' n mark the message as read
+ `trash' y trash the message to some folder
+ `unflag' n mark this message for unflagging
+ `untrash' n remove the `trashed' flag from a message
+ `unmark' n unmark this message
+ `unread' n mark the message as unread
+ `action' y mark the message for some action."
+ (interactive)
+ (let* ((msg (mu4e-message-at-point))
+ (docid (mu4e-message-field msg :docid))
+ ;; get a cell with the mark char and the "move" already has a target
+ ;; (the target folder) the other ones get a pseudo "target", as info
+ ;; for the user.
+ (markdesc (cdr (or (assq mark mu4e-marks)
+ (mu4e-error "Invalid mark %S" mark))))
+ (get-markkar
+ (lambda (char)
+ (if (listp char)
+ (if mu4e-use-fancy-chars (cdr char) (car char))
+ char)))
+ (markkar (funcall get-markkar (plist-get markdesc :char)))
+ (target (mu4e--mark-get-dyn-target mark target))
+ (show-fct (plist-get markdesc :show-target))
+ (shown-target (if show-fct
+ (funcall show-fct target)
+ (if target (format "%S" target)))))
+ (unless docid (mu4e-warn "No message on this line"))
+ (unless (eq major-mode 'mu4e-headers-mode)
+ (mu4e-error "Not in headers-mode"))
+ (save-excursion
+ (when (mu4e~headers-mark docid markkar)
+ ;; update the hash -- remove everything current, and if add the new
+ ;; stuff, unless we're unmarking
+ (remhash docid mu4e--mark-map)
+ ;; remove possible mark overlays
+ (remove-overlays (line-beginning-position) (line-end-position)
+ 'mu4e-mark t)
+ ;; now, let's set a mark (unless we were unmarking)
+ (unless (eql mark 'unmark)
+ (puthash docid (cons mark target) mu4e--mark-map)
+ ;; when we have a target (ie., when moving), show the target folder in
+ ;; an overlay
+ (when (and shown-target mu4e-headers-show-target)
+ (let* ((targetstr (propertize (concat "-> " shown-target " ")
+ 'face 'mu4e-system-face))
+ ;; mu4e~headers-goto-docid docid t \will take us just after
+ ;; the docid cookie and then we skip the mu4e--mark-fringe
+ (start (+ (length mu4e--mark-fringe)
+ (mu4e~headers-goto-docid docid t)))
+ (overlay (make-overlay start (+ start (length targetstr)))))
+ (overlay-put overlay 'display targetstr)
+ (overlay-put overlay 'mu4e-mark t)
+ (overlay-put overlay 'evaporate t)
+ docid)))))))
+
+(defun mu4e--mark-get-move-target ()
+ "Ask for a move target, and propose to create it if it does not exist."
+ (interactive)
+ ;; (mu4e-message-at-point) ;; raises error if there is none
+ (let* ((target (mu4e-ask-maildir "Move message to: "))
+ (target (if (string= (substring target 0 1) "/")
+ target
+ (concat "/" target)))
+ (fulltarget (concat (mu4e-root-maildir) target)))
+ (when (or (file-directory-p fulltarget)
+ (and (yes-or-no-p
+ (format "%s does not exist. Create now?" fulltarget))
+ (mu4e--server-mkdir fulltarget)))
+ target)))
+
+(defun mu4e--mark-ask-target (mark)
+ "Ask the target for MARK, if the user should be asked the target."
+ (let ((getter (plist-get (cdr (assq mark mu4e-marks)) :ask-target)))
+ (and getter (funcall getter))))
+
+(defun mu4e--mark-get-dyn-target (mark target)
+ "Get the dynamic TARGET for MARK.
+The result may depend on the message at point."
+ (let ((getter (plist-get (cdr (assq mark mu4e-marks)) :dyn-target)))
+ (if getter
+ (funcall getter target (mu4e-message-at-point))
+ target)))
+
+(defun mu4e-mark-set (mark &optional target)
+ "Mark the header at point with MARK or all in the region.
+Optionally, provide TARGET (for moves)."
+ (unless target
+ (setq target (mu4e--mark-ask-target mark)))
+ (if (not (use-region-p))
+ ;; single message
+ (mu4e-mark-at-point mark target)
+ ;; mark all messages in the region.
+ (save-excursion
+ (let ((cant-go-further) (eor (region-end)))
+ (goto-char (region-beginning))
+ (while (and (< (point) eor) (not cant-go-further))
+ (mu4e-mark-at-point mark target)
+ (setq cant-go-further (not (mu4e-headers-next))))))))
+
+(defun mu4e-mark-restore (docid)
+ "Restore the visual mark for the message with DOCID."
+ (let ((markcell (gethash docid mu4e--mark-map)))
+ (when markcell
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-mark-at-point (car markcell) (cdr markcell)))))))
+
+(defun mu4e--mark-get-markpair (prompt &optional allow-something)
+ "Ask user with PROMPT for a mark and return (MARK . TARGET).
+If ALLOW-SOMETHING is non-nil, allow the `something' pseudo mark
+as well."
+ (let* ((marks (mapcar (lambda (markdescr)
+ (cons (plist-get (cdr markdescr) :prompt)
+ (car markdescr)))
+ mu4e-marks))
+ (marks
+ (if allow-something
+ marks (seq-remove (lambda (m) (eq 'something (cdr m))) marks)))
+ (mark (mu4e-read-option prompt marks))
+ (target (mu4e--mark-ask-target mark)))
+ (cons mark target)))
+
+(defun mu4e-mark-resolve-deferred-marks ()
+ "Check if there are any deferred ('something') mark-instances.
+If there are such marks, replace them with a _real_ mark (ask the
+user which one)."
+ (interactive)
+ (mu4e--mark-in-context
+ (let ((markpair))
+ (maphash
+ (lambda (docid val)
+ (let ((mark (car val)))
+ (when (eql mark 'something)
+ (unless markpair
+ (setq markpair
+ (mu4e--mark-get-markpair "Set deferred mark(s) to: " nil)))
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-mark-set (car markpair) (cdr markpair)))))))
+ mu4e--mark-map))))
+
+(defun mu4e--mark-check-target (target)
+ "Check if TARGET exists; if not, offer to create it."
+ (let ((fulltarget (concat (mu4e-root-maildir) target)))
+ (if (not (mu4e-create-maildir-maybe fulltarget))
+ (mu4e-error "Target dir %s does not exist " fulltarget)
+ target)))
+
+(defun mu4e-mark-execute-all (&optional no-confirmation)
+ "Execute the actions for all marked messages in this buffer.
+After the actions have been executed successfully, the affected
+messages are *hidden* from the current header list. Since the
+headers are the result of a search, we cannot be certain that the
+messages no longer match the current one - to get that
+certainty, we need to rerun the search, but we don't want to do
+that automatically, as it may be too slow and/or break the user's
+flow. Therefore, we hide the message, which in practice seems to
+work well.
+
+If NO-CONFIRMATION is non-nil, don't ask user for confirmation."
+ (interactive "P")
+ (mu4e--mark-in-context
+ (let* ((marknum (hash-table-count mu4e--mark-map))
+ (prompt (format "Are you sure you want to execute %d mark%s?"
+ marknum (if (> marknum 1) "s" ""))))
+ (if (zerop marknum)
+ (mu4e-warn "Nothing is marked")
+ (mu4e-mark-resolve-deferred-marks)
+ (when (or no-confirmation (y-or-n-p prompt))
+ (maphash
+ (lambda (docid val)
+ (let* ((mark (car val)) (target (cdr val))
+ (markdescr (assq mark mu4e-marks))
+ (msg (save-excursion
+ (mu4e~headers-goto-docid docid)
+ (mu4e-message-at-point))))
+ ;; note: whenever you do something with the message,
+ ;; it looses its N (new) flag
+ (if markdescr
+ (progn
+ (run-hook-with-args
+ 'mu4e-mark-execute-pre-hook mark msg)
+ (funcall (plist-get (cdr markdescr) :action)
+ docid msg target))
+ (mu4e-error "Unrecognized mark %S" mark))))
+ mu4e--mark-map))
+ (mu4e-mark-unmark-all)
+ (message nil)))))
+
+(defun mu4e-mark-unmark-all ()
+ "Unmark all marked messages."
+ (interactive)
+ (mu4e--mark-in-context
+ (when (or (null mu4e--mark-map) (zerop (hash-table-count mu4e--mark-map)))
+ (mu4e-warn "Nothing is marked"))
+ (maphash
+ (lambda (docid _val)
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-mark-set 'unmark))))
+ mu4e--mark-map)
+ ;; in any case, clear the marks map
+ (mu4e--mark-clear)))
+
+(defun mu4e-mark-docid-marked-p (docid)
+ "Is the given DOCID marked?"
+ (when (gethash docid mu4e--mark-map) t))
+
+
+(defun mu4e-mark-marks-num ()
+ "Return the number of mark-instances in the current buffer."
+ (mu4e--mark-in-context
+ (if mu4e--mark-map (hash-table-count mu4e--mark-map) 0)))
+
+(defun mu4e-mark-handle-when-leaving ()
+ "Handle any mark-instances in the current buffer when leaving.
+This is done according to the value of
+`mu4e-headers-leave-behavior'. This function is to be called
+before any further action (like searching, quitting the buffer)
+is taken; returning t means \"take the following action\", return
+nil means \"don't do anything\"."
+ (mu4e--mark-in-context
+ (let ((marknum (mu4e-mark-marks-num))
+ (what mu4e-headers-leave-behavior))
+ (unless (zerop marknum) ;; nothing to do?
+ (when (eq what 'ask)
+ (setq what (mu4e-read-option
+ (format "There are %d existing mark(s); should we: "
+ marknum)
+ '( ("apply marks" . apply)
+ ("ignore marks?" . ignore)))))
+ ;; we determined what to do... now do it
+ (when (eq what 'apply)
+ (mu4e-mark-execute-all t))))))
+
+;;; _
+(provide 'mu4e-mark)
+;;; mu4e-mark.el ends here
--- /dev/null
+;;; mu4e-message.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Functions to get data from mu4e-message plist structure
+
+;;; Code:
+
+(require 'mu4e-vars)
+(require 'mu4e-contacts)
+(require 'flow-fill)
+(require 'shr)
+(require 'pp)
+
+(declare-function mu4e-error "mu4e-helpers")
+(declare-function mu4e-warn "mu4e-helpers")
+(declare-function mu4e-personal-address-p "mu4e-contacts")
+(declare-function mu4e-make-temp-file "mu4e-helpers")
+
+(make-obsolete-variable 'mu4e-html2text-command "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-view-prefer-html "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-view-html-plaintext-ratio-heuristic
+ "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-message-body-rewrite-functions
+ "No longer in use" "1.7.0")
+
+;;; Message fields
+
+(defsubst mu4e-message-field-raw (msg field)
+ "Retrieve FIELD from message plist MSG.
+
+See \"mu fields\" for the full list of field, in particular the
+\"sexp\" column.
+
+Returns nil if the field does not exist.
+
+A message plist looks something like:
+\(:docid 32461
+ :from ((:name \"Nikola Tesla\" :email \"niko@example.com\"))
+ :to ((:name \"Thomas Edison\" :email \"tom@example.com\"))
+ :cc ((:name \"Rupert The Monkey\" :email \"rupert@example.com\"))
+ :subject \"RE: what about the 50K?\"
+ :date (20369 17624 0)
+ :size 4337
+ :message-id \"238C8233AB82D81EE81AF0114E4E74@123213.mail.example.com\"
+ :path \"/home/tom/Maildir/INBOX/cur/133443243973_1.10027.atlas:2,S\"
+ :maildir \"/INBOX\"
+ :priority normal
+ :flags (seen)
+\)).
+Some notes on the format:
+- The address fields are lists of plist (:name NAME :email EMAIL),
+ where the :name part can be absent. The `mu4e-contact-name' and
+ `mu4e-contact-email' accessors can be useful for this.
+- The date is in format emacs uses in `current-time'
+- Attachments are a list of elements with fields :index (the number of
+ the MIME-part), :name (the file name, if any), :mime-type (the
+ MIME-type, if any) and :size (the size in bytes, if any).
+- Messages in the Headers view come from the database and do not have
+ :attachments, :body-txt or :body-html fields. Message in the
+ Message view use the actual message file, and do include these fields."
+ ;; after all this documentation, the spectacular implementation
+ (if msg
+ (plist-get msg field)
+ (mu4e-error "Message must be non-nil")))
+
+(defsubst mu4e-message-field (msg field)
+ "Retrieve FIELD from message plist MSG.
+Like `mu4e-message-field-nil', but will sanitize nil values:
+- all string field except body-txt/body-html: nil -> \"\"
+- numeric fields + dates : nil -> 0
+- all others : return the value
+Thus, function will return nil for empty lists, non-existing body-txt
+or body-html."
+ (let ((val (mu4e-message-field-raw msg field)))
+ (cond
+ (val
+ val) ;; non-nil -> just return it
+ ((member field '(:subject :message-id :path :maildir :in-reply-to))
+ "") ;; string fields except body-txt, body-html: nil -> ""
+ ((member field '(:body-html :body-txt))
+ val)
+ ((member field '(:docid :size))
+ 0) ;; numeric type: nil -> 0
+ (t
+ val)))) ;; otherwise, just return nil
+
+(defsubst mu4e-message-has-field (msg field)
+ "If MSG has a FIELD return t, nil otherwise."
+ (plist-member msg field))
+
+(defsubst mu4e-message-at-point (&optional noerror)
+ "Get the message s-expression for the message at point.
+Either the headers buffer or the view buffer, or nil if there is
+no such message. If optional NOERROR is non-nil, do not raise an
+error when there is no message at point."
+ (or (cond
+ ((eq major-mode 'mu4e-headers-mode) (get-text-property (point) 'msg))
+ ((eq major-mode 'mu4e-view-mode) mu4e~view-message))
+ (unless noerror (mu4e-warn "No message at point"))))
+
+(defsubst mu4e-message-field-at-point (field)
+ "Get the field FIELD from the message at point.
+This is equivalent to:
+ (mu4e-message-field (mu4e-message-at-point) FIELD)."
+ (mu4e-message-field (mu4e-message-at-point) field))
+
+(defun mu4e-message-contact-field-matches (msg cfield rx)
+ "Does MSG's contact-field CFIELD match regexp RX?
+Check if any of the of the CFIELD in MSG matches RX. I.e.
+anything in field CFIELD (either :to, :from, :cc or :bcc, or a
+list of those) of msg MSG matches (with their name or e-mail
+address) regular expressions RX. If there is a match, return
+non-nil; otherwise return nil. RX can also be a list of regular
+expressions, in which case any of those are tried for a match."
+ (cond
+ ((null cfield))
+ ((listp cfield)
+ (seq-find (lambda (cf) (mu4e-message-contact-field-matches msg cf rx))
+ cfield))
+ ((listp rx)
+ ;; if rx is a list, try each one of them for a match
+ (seq-find
+ (lambda (a-rx) (mu4e-message-contact-field-matches msg cfield a-rx))
+ rx))
+ (t
+ ;; not a list, check the rx
+ (seq-find
+ (lambda (ct)
+ (let ((name (mu4e-contact-name ct))
+ (email (mu4e-contact-email ct))
+ ;; the 'rx' may be some `/rx/` from mu4e-personal-addresses;
+ ;; so let's detect and extract in that case.
+ (rx (if (string-match-p "^\\(.*\\)/$" rx)
+ (substring rx 1 -1) rx)))
+ (or
+ (and name (string-match rx name))
+ (and email (string-match rx email)))))
+ (mu4e-message-field msg cfield)))))
+
+(defun mu4e-message-contact-field-matches-me (msg cfield)
+ "Does contact-field CFIELD in MSG match me?
+Checks whether any
+of the of the contacts in field CFIELD (either :to, :from, :cc or
+:bcc) of msg MSG matches *me*, that is, any of the addresses for
+which `mu4e-personal-address-p' return t. Returns the contact
+cell that matched, or nil."
+ (seq-find (lambda (cell)
+ (mu4e-personal-address-p (mu4e-contact-email cell)))
+ (mu4e-message-field msg cfield)))
+
+(defun mu4e-message-sent-by-me (msg)
+ "Is this MSG (to be) sent by me?
+Checks if the from field matches user's personal addresses."
+ (mu4e-message-contact-field-matches-me msg :from))
+
+(defun mu4e-message-personal-p (msg)
+ "Does MSG have user's personal address?
+In any of the contact
+ fields?"
+ (seq-some
+ (lambda (field)
+ (mu4e-message-contact-field-matches-me msg field))
+ '(:from :to :cc :bcc)))
+
+(defsubst mu4e-message-part-field (msgpart field)
+ "Get some FIELD from MSGPART.
+A part would look something like:
+ (:index 2 :name \"photo.jpg\" :mime-type \"image/jpeg\" :size 147331)."
+ (plist-get msgpart field))
+
+;; backward compatibility ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(defalias 'mu4e-msg-field 'mu4e-message-field)
+
+(defun mu4e-field-at-point (field)
+ "Get FIELD for the message at point.
+Either in the headers buffer or the view buffer. Field is a
+symbol, see `mu4e-header-info'."
+ (plist-get (mu4e-message-at-point) field))
+
+;;; Html2Text
+(make-obsolete 'mu4e-shr2text "No longer in use" "1.7.0")
+
+(defun mu4e-message-readable-path (&optional msg)
+ "Get a readable path to MSG or raise an error.
+If MSG is nil, use `mu4e-message-at-point'."
+ (let ((path (plist-get (or msg (mu4e-message-at-point)) :path)))
+ (unless (file-readable-p path)
+ (mu4e-error "No readable message at %s; database outdated?" path))
+ path))
+
+(defun mu4e-copy-message-path ()
+ "Copy the message-path of message at point to the kill ring."
+ (interactive)
+ (let ((path (mu4e-message-field-at-point :path)))
+ (kill-new path)
+ (mu4e-message "Saved '%s' to kill-ring" path)))
+
+(defconst mu4e--sexp-buffer-name " *mu4e-sexp-at-point"
+ "Buffer name for sexp buffers.")
+
+(defun mu4e-sexp-at-point ()
+ "Show or hide the s-expression for the message-at-point, if any."
+ (interactive)
+ (if-let ((win (get-buffer-window mu4e--sexp-buffer-name)))
+ (delete-window win)
+ (when-let ((msg (mu4e-message-at-point 'noerror)))
+ (with-current-buffer-window mu4e--sexp-buffer-name nil nil
+ ;; the "pretty-printing" is not very pretty...
+ (insert (pp-to-string msg))))))
+
+;;;
+(provide 'mu4e-message)
+;;; mu4e-message.el ends here
--- /dev/null
+;;; mu4e-org -- Org-links to mu4e messages/queries -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: outlines, hypermedia, calendar, mail
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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 1the License, or
+;; (at your option) any later version.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The expect version here is org 9.x.
+
+;;; Code:
+
+(require 'org)
+(require 'mu4e-view)
+(require 'mu4e-contacts)
+
+
+(defgroup mu4e-org nil
+ "Settings for the Org mode related functionality in mu4e."
+ :group 'mu4e
+ :group 'org)
+
+(defcustom mu4e-org-link-desc-func
+ (lambda (msg) (or (plist-get msg :subject) "No subject"))
+ "Function that takes a msg and returns a description.
+This can be used in org capture templates and storing links.
+
+Example usage:
+
+ (defun my-link-descr (msg)
+ (let ((subject (or (plist-get msg :subject)
+ \"No subject\"))
+ (date (or (format-time-string mu4e-headers-date-format
+ (mu4e-msg-field msg :date))
+ \"No date\")))
+ (concat subject \" \" date)))
+
+ (setq org-mu4e-link-desc-func \='my-link-descr)"
+ :type '(function)
+ :group 'mu4e-org)
+
+(defvar mu4e-org-link-query-in-headers-mode nil
+ "Prefer linking to the query rather than to the message.
+If non-nil, `org-store-link' in `mu4e-headers-mode' links to the
+the current query; otherwise, it links to the message at point.")
+
+(defun mu4e--org-store-link-query ()
+ "Store a link to a mu4e query."
+ (setq org-store-link-plist nil) ; reset
+ (org-store-link-props
+ :type "mu4e"
+ :query (mu4e-last-query)
+ :date (format-time-string "%FT%T") ;; avoid error
+ :link (concat "mu4e:query:" (mu4e-last-query))
+ :description (format "[%s]" (mu4e-last-query))))
+
+(defun mu4e--org-store-link-message ()
+ "Store a link to a mu4e message."
+ (setq org-store-link-plist nil)
+ (let* ((msg (mu4e-message-at-point))
+ (from (car-safe (plist-get msg :from)))
+ (to (car-safe (plist-get msg :to)))
+ (date (format-time-string "%FT%T" (plist-get msg :date)))
+ (msgid (or (plist-get msg :message-id)
+ (mu4e-error "Cannot link message without message-id")))
+ (props `(:type "mu4e"
+ :date ,date
+ :from ,(mu4e-contact-full from)
+ :fromname ,(mu4e-contact-name from)
+ :fromnameoraddress ,(or (mu4e-contact-name from)
+ (mu4e-contact-email from)) ;; mu4e-specific
+ :maildir ,(plist-get msg :maildir)
+ :message-id ,msgid
+ :path ,(plist-get msg :path)
+ :subject ,(plist-get msg :subject)
+ :to ,(mu4e-contact-full to)
+ :tonameoraddress ,(or (mu4e-contact-name to)
+ (mu4e-contact-email to)) ;; mu4e-specific
+ :link ,(concat "mu4e:msgid:" msgid)
+ :description ,(funcall mu4e-org-link-desc-func msg))))
+ (apply #'org-store-link-props props)))
+
+(defun mu4e-org-store-link ()
+ "Store a link to a mu4e message or query.
+It links to the last known query when in `mu4e-headers-mode' with
+`mu4e-org-link-query-in-headers-mode' set; otherwise it links to
+a specific message, based on its message-id, so that links stay
+valid even after moving the message around."
+ (cond
+ ((mu4e-is-mode-or-derived-p 'mu4e-view-mode) (mu4e--org-store-link-message))
+ ((mu4e-is-mode-or-derived-p 'mu4e-headers-mode)
+ (if mu4e-org-link-query-in-headers-mode
+ (mu4e--org-store-link-query)
+ (mu4e--org-store-link-message)))))
+
+(defun mu4e-org-open (link)
+ "Open the org LINK.
+Open the mu4e message (for links starting with \"msgid:\") or run
+the query (for links starting with \"query:\")."
+ (require 'mu4e)
+ (cond
+ ((string-match "^msgid:\\(.+\\)" link)
+ (mu4e-view-message-with-message-id (match-string 1 link)))
+ ((string-match "^query:\\(.+\\)" link)
+ (mu4e-search (match-string 1 link) current-prefix-arg))
+ (t (mu4e-error "Unrecognized link type '%s'" link))))
+
+(make-obsolete 'org-mu4e-open 'mu4e-org-open "1.3.6")
+
+(defun mu4e-org-store-and-capture ()
+ "Store a link to the current message or query.
+\(depending on `mu4e-org-link-query-in-headers-mode', and capture
+it with org)."
+ (interactive)
+ (call-interactively 'org-store-link)
+ (org-capture))
+
+(make-obsolete 'org-mu4e-store-and-capture
+ 'mu4e-org-store-and-capture "1.3.6")
+
+;; install mu4e-link support.
+(org-link-set-parameters "mu4e"
+ :follow #'mu4e-org-open
+ :store #'mu4e-org-store-link)
+(provide 'mu4e-org)
+;;; mu4e-org.el ends here
--- /dev/null
+;;; mu4e-search.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021,2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Search-related functions and a minor-mode.
+
+;;; Code:
+
+(require 'seq)
+(require 'mu4e-helpers)
+(require 'mu4e-message)
+(require 'mu4e-bookmarks)
+(require 'mu4e-contacts)
+(require 'mu4e-lists)
+(require 'mu4e-mark)
+
+\f
+;;; Configuration
+(defgroup mu4e-search nil
+ "Search-related settings."
+ :group 'mu4e)
+
+(define-obsolete-variable-alias 'mu4e-headers-results-limit
+ 'mu4e-search-results-limit "1.7.0")
+(defcustom mu4e-search-results-limit 500
+ "Maximum number of results to show.
+This affects performance, especially when
+`mu4e-summary-include-related' is non-nil.
+Set to -1 for no limits."
+ :type '(choice (const :tag "Unlimited" -1)
+ (integer :tag "Limit"))
+ :group 'mu4e-search)
+
+(define-obsolete-variable-alias 'mu4e-headers-full-search
+ 'mu4e-search-full "1.7.0")
+(defvar mu4e-search-full nil
+ "Whether to search for all results.
+If this is nil, search for up to `mu4e-search-results-limit')")
+
+
+(define-obsolete-variable-alias 'mu4e-headers-show-threads
+ 'mu4e-search-threads "1.7.0")
+(defvar mu4e-search-threads t
+ "Whether to calculate threads for the search results.")
+
+(defcustom mu4e-query-rewrite-function 'identity
+ "Function to rewrite a query.
+
+It takes a search expression string, and returns a possibly
+ changed search expression string.
+
+This function is applied on the search expression just before
+searching, and allows users to modify the query.
+
+For instance, we could change and of workmail into
+\"maildir:/long-path-to-work-related-emails\", by setting the function
+
+(setq mu4e-query-rewrite-function
+ (lambda(expr)
+ (replace-regexp-in-string \"workmail\"
+ \"maildir:/long-path-to-work-related-emails\" expr)))
+
+It is good to remember that the replacement does not understand
+anything about the query, it just does text replacement."
+ :type 'function
+ :group 'mu4e-search)
+
+(define-obsolete-variable-alias
+ 'mu4e-headers-search-bookmark-hook
+ 'mu4e-search-bookmark-hook "1.7.0")
+(defcustom mu4e-search-bookmark-hook nil
+ "Hook run just after invoking a bookmarked search.
+
+This function receives the query as its parameter, before any
+rewriting as per `mu4e-query-rewrite-function' has taken place.
+
+The reason to use this instead of `mu4e-headers-search-hook' is
+if you only want to execute a hook when a search is entered via a
+bookmark, e.g. if you'd like to treat the bookmarks as a custom
+folder and change the options for the search."
+ :type 'hook
+ :group 'mu4e-search)
+
+(define-obsolete-variable-alias 'mu4e-headers-search-hook
+ 'mu4e-search-hook "1.7.0")
+(defcustom mu4e-search-hook nil
+ "Hook run just before executing a new search operation.
+This function receives the query as its parameter, before any
+rewriting as per `mu4e-query-rewrite-function' has taken place
+
+This is a more general hook facility than the
+`mu4e-search-bookmark-hook'. It gets called on every
+executed search, not just those that are invoked via bookmarks,
+but also manually invoked searches."
+ :type 'hook
+ :group 'mu4e-search)
+\f
+;; Internals
+
+;;; History
+(defvar mu4e--search-query-past nil
+ "Stack of queries before the present one.")
+(defvar mu4e--search-query-future nil
+ "Stack of queries after the present one.")
+(defvar mu4e--search-query-stack-size 20
+ "Maximum size for the query stacks.")
+(defvar mu4e--search-last-query nil
+ "The present (most recent) query.")
+
+
+\f
+;;; Interactive functions
+(declare-function mu4e--search-execute "mu4e-headers")
+
+(defvar mu4e--search-view-target nil
+ "Whether to automatically view (open) the target message.")
+(defvar mu4e--search-msgid-target nil
+ "Message-id to jump to after the search has finished.")
+
+
+(defun mu4e-search (&optional expr prompt edit ignore-history msgid show)
+ "Search for query EXPR.
+
+Switch to the output buffer for the results. This is an
+interactive function which ask user for EXPR. PROMPT, if non-nil,
+is the prompt used by this function (default is \"Search for:\").
+If EDIT is non-nil, instead of executing the query for EXPR, let
+the user edit the query before executing it.
+
+If IGNORE-HISTORY is true, do *not* update the query history
+stack. If MSGID is non-nil, attempt to move point to the first
+message with that message-id after searching. If SHOW is non-nil,
+show the message with MSGID."
+ (interactive)
+ (let* ((prompt (mu4e-format (or prompt "Search for: ")))
+ (expr
+ (if (or (null expr) edit)
+ (mu4e-read-query prompt expr)
+ expr)))
+ (mu4e-mark-handle-when-leaving)
+ (mu4e--search-execute expr ignore-history)
+ (setq mu4e--search-msgid-target msgid
+ mu4e--search-view-target show)))
+
+(define-obsolete-function-alias 'mu4e-headers-search 'mu4e-search "1.7.0")
+
+(defun mu4e-search-edit ()
+ "Edit the last search expression."
+ (interactive)
+ (mu4e-search mu4e--search-last-query nil t))
+
+(define-obsolete-function-alias 'mu4e-headers-search-edit
+ 'mu4e-search-edit "1.7.0")
+
+(defun mu4e-search-bookmark (&optional expr edit)
+ "Search using some bookmarked query EXPR.
+If EDIT is non-nil, let the user edit the bookmark before starting
+the search."
+ (interactive)
+ (let ((expr
+ (or expr
+ (mu4e-ask-bookmark (if edit "Select bookmark: " "Bookmark: ")))))
+ (run-hook-with-args 'mu4e-search-bookmark-hook expr)
+ (mu4e-search expr (when edit "Edit bookmark: ") edit)))
+
+(define-obsolete-function-alias 'mu4e-headers-search-bookmark
+ 'mu4e-search-bookmark "1.7.0")
+
+(defun mu4e-search-bookmark-edit ()
+ "Edit an existing bookmark before executing it."
+ (interactive)
+ (mu4e-search-bookmark nil t))
+
+(define-obsolete-function-alias 'mu4e-headers-search-bookmark-edit
+ 'mu4e-search-bookmark-edit "1.7.0")
+
+(defun mu4e-search-narrow(&optional filter)
+ "Narrow the last search.
+Do so by appending search expression FILTER to the last search
+expression. Note that you can go back to the previous
+query (effectively, \"widen\" it), with `mu4e-search-prev'."
+ (interactive
+ (let ((filter
+ (read-string (mu4e-format "Narrow down to: ")
+ nil 'mu4e~headers-search-hist nil t)))
+ (list filter)))
+ (unless mu4e--search-last-query
+ (mu4e-warn "There's nothing to filter"))
+ (mu4e-search (format "(%s) AND (%s)" mu4e--search-last-query filter)))
+
+(define-obsolete-function-alias 'mu4e-headers-search-narrow
+ 'mu4e-search-narrow "1.7.0")
+
+;; (defun mu4e-headers-change-sorting (&optional field dir)
+;; "Change the sorting/threading parameters.
+;; FIELD is the field to sort by; DIR is a symbol: either 'ascending,
+;; 'descending, 't (meaning: if FIELD is the same as the current
+;; sortfield, change the sort-order) or nil (ask the user)."
+;; (interactive)
+;; (let* ((field
+;; (or field
+;; (mu4e-read-option "Sortfield: " mu4e~headers-sort-field-choices)))
+;; ;; note: 'sortable' is either a boolean (meaning: if non-nil, this is
+;; ;; sortable field), _or_ another field (meaning: sort by this other field).
+;; (sortable (plist-get (cdr (assoc field mu4e-header-info)) :sortable))
+;; ;; error check
+;; (sortable
+;; (if sortable
+;; sortable
+;; (mu4e-error "Not a sortable field")))
+;; (sortfield (if (booleanp sortable) field sortable))
+;; (dir
+;; (cl-case dir
+;; ((ascending descending) dir)
+;; ;; change the sort order if field = curfield
+;; (t
+;; (if (eq sortfield mu4e-headers-sort-field)
+;; (if (eq mu4e-headers-sort-direction 'ascending)
+;; 'descending 'ascending)
+;; 'descending))
+;; (mu4e-read-option "Direction: "
+;; '(("ascending" . 'ascending) ("descending" . 'descending))))))
+;; (setq
+;; mu4e-headers-sort-field sortfield
+;; mu4e-headers-sort-direction dir)
+;; (mu4e-message "Sorting by %s (%s)"
+;; (symbol-name sortfield)
+;; (symbol-name mu4e-headers-sort-direction))
+;; (mu4e-headers-rerun-search)))
+
+;; (defun mu4e~headers-toggle (name togglevar dont-refresh)
+;; "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil,
+;; re-run the last search."
+;; (set togglevar (not (symbol-value togglevar)))
+;; (mu4e-message "%s turned %s%s"
+;; name
+;; (if (symbol-value togglevar) "on" "off")
+;; (if dont-refresh
+;; " (press 'g' to refresh)" ""))
+;; (unless dont-refresh
+;; (mu4e-headers-rerun-search)))
+
+;; (defun mu4e-headers-toggle-threading (&optional dont-refresh)
+;; "Toggle `mu4e-headers-show-threads'. With prefix-argument, do
+;; _not_ refresh the last search with the new setting for threading."
+;; (interactive "P")
+;; (mu4e~headers-toggle "Threading" 'mu4e-headers-show-threads dont-refresh))
+
+;; (defun mu4e-headers-toggle-full-search (&optional dont-refresh)
+;; "Toggle `mu4e-headers-full-search'. With prefix-argument, do
+;; _not_ refresh the last search with the new setting for threading."
+;; (interactive "P")
+;; (mu4e~headers-toggle "Full-search"
+;; 'mu4e-headers-full-search dont-refresh))
+
+;; (defun mu4e-headers-toggle-include-related (&optional dont-refresh)
+;; "Toggle `mu4e-headers-include-related'. With prefix-argument, do
+;; _not_ refresh the last search with the new setting for threading."
+;; (interactive "P")
+;; (mu4e~headers-toggle "Include-related"
+;; 'mu4e-headers-include-related dont-refresh))
+
+;; (defun mu4e-headers-toggle-skip-duplicates (&optional dont-refresh)
+;; "Toggle `mu4e-headers-skip-duplicates'. With prefix-argument, do
+;; _not_ refresh the last search with the new setting for threading."
+;; (interactive "P")
+;; (mu4e~headers-toggle "Skip-duplicates"
+;; 'mu4e-headers-skip-duplicates dont-refresh))
+
+\f
+
+(defun mu4e--search-push-query (query where)
+ "Push QUERY to one of the query stacks.
+WHERE is a symbol telling us where to push; it's a symbol, either
+`future' or `past'. Also removes duplicates and truncates to
+limit the stack size."
+ (let ((stack
+ (pcase where
+ ('past mu4e--search-query-past)
+ ('future mu4e--search-query-future))))
+ ;; only add if not the same item
+ (unless (and stack (string= (car stack) query))
+ (push query stack)
+ ;; limit the stack to `mu4e--search-query-stack-size' elements
+ (when (> (length stack) mu4e--search-query-stack-size)
+ (setq stack (cl-subseq stack 0 mu4e--search-query-stack-size)))
+ ;; remove all duplicates of the new element
+ (seq-remove (lambda (elm) (string= elm (car stack))) (cdr stack))
+ ;; update the stacks
+ (pcase where
+ ('past (setq mu4e--search-query-past stack))
+ ('future (setq mu4e--search-query-future stack))))))
+
+(defun mu4e--search-pop-query (whence)
+ "Pop a query from the stack.
+WHENCE is a symbol telling us where to get it from, either `future'
+or `past'."
+ (pcase whence
+ ('past
+ (unless mu4e--search-query-past
+ (mu4e-warn "No more previous queries"))
+ (pop mu4e--search-query-past))
+ ('future
+ (unless mu4e--search-query-future
+ (mu4e-warn "No more next queries"))
+ (pop mu4e--search-query-future))))
+
+(defun mu4e-search-rerun ()
+ "Re-run the search for the last search expression."
+ (interactive)
+ ;; if possible, try to return to the same message
+ (let* ((msg (mu4e-message-at-point t))
+ (msgid (and msg (mu4e-message-field msg :message-id))))
+ (mu4e-search mu4e--search-last-query nil nil t msgid)))
+
+(define-obsolete-function-alias 'mu4e-headers-rerun-search
+ 'mu4e-search-rerun "1.7.0")
+
+(defun mu4e--search-query-navigate (whence)
+ "Execute the previous query from the query stacks.
+WHENCE determines where the query is taken from and is a symbol,
+either `future' or `past'."
+ (let ((query (mu4e--search-pop-query whence))
+ (where (if (eq whence 'future) 'past 'future)))
+ (when query
+ (mu4e--search-push-query mu4e--search-last-query where)
+ (mu4e-search query nil nil t))))
+
+(defun mu4e-search-next ()
+ "Execute the next query from the query stack."
+ (interactive)
+ (mu4e--search-query-navigate 'future))
+
+(define-obsolete-function-alias 'mu4e-headers-query-next
+ 'mu4e-search-next "1.7.0")
+
+(defun mu4e-search-prev ()
+ "Execute the previous query from the query stacks."
+ (interactive)
+ (mu4e--search-query-navigate 'past))
+
+(define-obsolete-function-alias 'mu4e-headers-query-prev
+ 'mu4e-search-prev "1.7.0")
+
+;; forget the past so we don't repeat it :/
+(defun mu4e-search-forget ()
+ "Forget the search history."
+ (interactive)
+ (setq mu4e--search-query-past nil
+ mu4e--search-query-future nil)
+ (mu4e-message "Query history cleared"))
+
+(define-obsolete-function-alias 'mu4e-headers-forget-queries
+ 'mu4e-search-forget "1.7.0")
+
+(defun mu4e-last-query ()
+ "Get the most recent query or nil if there is none."
+ mu4e--search-last-query)
+\f
+;;; Completion for queries
+
+(defvar mu4e--search-hist nil "History list of searches.")
+(defvar mu4e-minibuffer-search-query-map
+ (let ((map (copy-keymap minibuffer-local-map)))
+ (define-key map (kbd "TAB") #'completion-at-point)
+ map)
+ "The keymap for reading a search query.")
+
+(defun mu4e-search-read-query (prompt &optional initial-input)
+ "Read a query with completion using PROMPT and INITIAL-INPUT."
+ (minibuffer-with-setup-hook
+ (lambda ()
+ (setq-local completion-at-point-functions
+ #'mu4e--search-query-completion-at-point)
+ (use-local-map mu4e-minibuffer-search-query-map))
+ (read-string prompt initial-input 'mu4e--search-hist)))
+
+(define-obsolete-function-alias 'mu4e-read-query
+ 'mu4e-search-read-query "1.7.0")
+
+(defconst mu4e--search-query-keywords
+ '("and" "or" "not"
+ "from:" "to:" "cc:" "bcc:" "contact:" "recip:" "date:" "subject:" "body:"
+ "list:" "maildir:" "flag:" "mime:" "file:" "prio:" "tag:" "msgid:"
+ "size:" "embed:"))
+
+(defun mu4e--search-completion-contacts-action (match _status)
+ "Delete contact alias from contact autocompletion, leaving just email address.
+Implements the `completion-extra-properties' :exit-function' which
+requires a function with arguments string MATCH and completion
+status, STATUS."
+ (let ((contact-email (replace-regexp-in-string "^.*<\\|>$" "" match)))
+ (delete-char (- (length match)))
+ (insert contact-email)))
+
+(defun mu4e--search-query-completion-at-point ()
+ "Provide completion when entering search expressions."
+ (cond
+ ((not (looking-back "[:\"][^ \t]*" nil))
+ (let ((bounds (bounds-of-thing-at-point 'word)))
+ (list (or (car bounds) (point))
+ (or (cdr bounds) (point))
+ mu4e--search-query-keywords)))
+ ((looking-back "flag:\\(\\w*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ '("attach" "draft" "flagged" "list" "new" "passed" "replied"
+ "seen" "trashed" "unread" "encrypted" "signed" "personal")))
+ ((looking-back "maildir:\\([a-zA-Z0-9/.]*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ (mu4e-get-maildirs)))
+ ((looking-back "prio:\\(\\w*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ (list "high" "normal" "low")))
+ ((looking-back "mime:\\([a-zA-Z0-9/-]*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ (mailcap-mime-types)))
+ ((looking-back "\\(from\\|to\\|cc\\|bcc\\|contact\\|recip\\):\\([a-zA-Z0-9/.@]*\\)" nil)
+ (list (match-beginning 2)
+ (match-end 2)
+ mu4e--contacts-set
+ :exit-function
+ #'mu4e--search-completion-contacts-action))
+ ((looking-back "list:\\([a-zA-Z0-9/.@]*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ mu4e--lists-hash))))
+
+(define-minor-mode mu4e-search-minor-mode
+ "Mode for searching for messages."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ :keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map "s" 'mu4e-search)
+ (define-key map "S" 'mu4e-search-edit)
+ (define-key map "/" 'mu4e-search-narrow)
+ ;;(define-key map "j" 'mu4e~headers-jump-to-maildir)
+ (define-key map (kbd "<M-left>") 'mu4e-search-prev)
+ (define-key map (kbd "\\") 'mu4e-search-prev)
+ (define-key map (kbd "<M-right>") 'mu4e-search-next)
+
+ (define-key map "b" 'mu4e-search-bookmark)
+ (define-key map "B" 'mu4e-search-bookmark-edit)
+ map))
+
+(provide 'mu4e-search)
+;;; mu4e-search.el ends here
--- /dev/null
+;;; mu4e-server.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'mu4e-helpers)
+
+\f
+;;; Configuration
+(defcustom mu4e-mu-home nil
+ "Location of an alternate mu home dir.
+If not set, use the defaults, based on the XDG Base Directory
+Specification.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+ :group 'mu4e
+ :type '(choice (const :tag "Default location" nil)
+ (directory :tag "Specify location"))
+ :safe 'stringp)
+
+(defcustom mu4e-mu-binary (executable-find "mu")
+ "Path to the mu-binary to use.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+ :type 'file
+ :group 'mu4e
+ :safe 'stringp)
+
+(defcustom mu4e-mu-debug nil
+ "Whether to run the mu binary in debug-mode.
+Setting this to t increases the amount of information in the log.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+ :type 'boolean
+ :group 'mu4e)
+
+(make-obsolete-variable
+ 'mu4e-maildir
+ "determined by server; see `mu4e-root-maildir'." "1.3.8")
+
+(defcustom mu4e-change-filenames-when-moving nil
+ "Change message file names when moving them.
+
+When moving messages to different folders, normally mu/mu4e keep
+the base filename the same (the flags-part of the filename may
+change still). With this option set to non-nil, mu4e instead
+changes the filename.
+
+This latter behavior works better with some
+IMAP-synchronization programs such as mbsync; the default works
+better with e.g. offlineimap."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+\f
+;; Handlers are not strictly internal, but are not meant
+;; for overriding outside mu4e. The are mainly for breaking
+;; dependency cycles.
+
+(defvar mu4e-error-func nil
+ "Function called for each error received.
+The function is passed an error plist as argument. See
+`mu4e--server-filter' for the format.")
+
+(defvar mu4e-update-func nil
+ "Function called for each :update sexp returned.
+The function is passed a msg sexp as argument.
+See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-remove-func nil
+ "Function called for each :remove sexp returned.
+This happens when some message has been deleted. The function is
+passed the docid of the removed message.")
+
+(defvar mu4e-sent-func nil
+ "Function called for each :sent sexp received.
+This happens when some message has been sent. The function is
+passed the docid and the draft-path of the sent message.")
+
+(defvar mu4e-view-func nil
+ "Function called for each single-message sexp.
+The function is passed a message sexp as argument. See
+`mu4e--server-filter' for the format.")
+
+(make-obsolete-variable 'mu4e-header-func "mu4e-headers-append-func" "1.7.4")
+
+(defvar mu4e-headers-append-func nil
+ "Function called with a list of headers to append.
+The function is passed a list of message plists as argument. See
+See `mu4e--server-filter' for the details.")
+
+(defvar mu4e-found-func nil
+ "Function called for when we received a :found sexp.
+This happens after the headers have been returned, to report on
+the number of matches. See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-erase-func nil
+ "Function called we receive an :erase sexp.
+This before new headers are displayed, to clear the current
+headers buffer. See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-compose-func nil
+ "Function called for each compose message received.
+I.e., the original message that is used as basis for composing a
+new message (i.e., either a reply or a forward); the function is
+passed msg and a symbol (either reply or forward). See
+`mu4e--server-filter' for the format of <msg-plist>.")
+
+(defvar mu4e-info-func nil
+ "Function called for each (:info type ....) sexp received.
+from the server process.")
+
+(defvar mu4e-pong-func nil
+ "Function called for each (:pong type ....) sexp received.")
+
+(defvar mu4e-contacts-func nil
+ "A function called for each (:contacts (<list-of-contacts>)
+sexp received from the server process.")
+
+(make-obsolete-variable 'mu4e-temp-func "No longer used" "1.7.0")
+\f
+;;; Internal vars
+
+(defvar mu4e--server-buf nil
+ "Buffer (string) for data received from the backend.")
+(defconst mu4e--server-name " *mu4e-server*"
+ "Name of the server process, buffer.")
+(defvar mu4e--server-process nil
+ "The mu-server process.")
+
+;; dealing with the length cookie that precedes expressions
+(defconst mu4e--server-cookie-pre "\376"
+ "Each expression starts with a length cookie:
+<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>.")
+(defconst mu4e--server-cookie-post "\377"
+ "Each expression starts with a length cookie:
+<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>.")
+(defconst mu4e--server-cookie-matcher-rx
+ (concat mu4e--server-cookie-pre "\\([[:xdigit:]]+\\)"
+ mu4e--server-cookie-post)
+ "Regular expression matching the length cookie.
+Match 1 will be the length (in hex).")
+\f
+(defun mu4e-running-p ()
+ "Whether mu4e is running.
+Checks whether the server process is live."
+ (and mu4e--server-process
+ (memq (process-status mu4e--server-process)
+ '(run open listen connect stop))
+ t))
+
+(defsubst mu4e--server-eat-sexp-from-buf ()
+ "'Eat' the next s-expression from `mu4e--server-buf'.
+Note: this is a string, not an emacs-buffer. `mu4e--server-buf gets
+its contents from the mu-servers in the following form:
+ <`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>
+Function returns this sexp, or nil if there was none.
+`mu4e--server-buf' is updated as well, with all processed sexp data
+removed."
+ (ignore-errors ;; the server may die in the middle...
+ (let ((b (string-match mu4e--server-cookie-matcher-rx mu4e--server-buf))
+ (sexp-len) (objcons))
+ (when b
+ (setq sexp-len (string-to-number (match-string 1 mu4e--server-buf) 16))
+ ;; does mu4e--server-buf contain the full sexp?
+ (when (>= (length mu4e--server-buf) (+ sexp-len (match-end 0)))
+ ;; clear-up start
+ (setq mu4e--server-buf (substring mu4e--server-buf (match-end 0)))
+ ;; note: we read the input in binary mode -- here, we take the part
+ ;; that is the sexp, and convert that to utf-8, before we interpret
+ ;; it.
+ (setq objcons (read-from-string
+ (decode-coding-string
+ (substring mu4e--server-buf 0 sexp-len)
+ 'utf-8 t)))
+ (when objcons
+ (setq mu4e--server-buf (substring mu4e--server-buf sexp-len))
+ (car objcons)))))))
+
+(defun mu4e--server-filter (_proc str)
+ "Filter string STR from PROC.
+This processes the \"mu server\" output. It accumulates the
+strings into valid sexpsv and evaluating those.
+
+The server output is as follows:
+
+ 1. an error
+ (:error 2 :message \"unknown command\")
+ ;; eox
+ => passed to `mu4e-error-func'.
+
+ 2a. a header exp looks something like:
+ (:headers
+ ( ;; message 1
+ :docid 1585
+ :from ((\"Donald Duck\" . \"donald@example.com\"))
+ :to ((\"Mickey Mouse\" . \"mickey@example.com\"))
+ :subject \"Wicked stuff\"
+ :date (20023 26572 0)
+ :size 15165
+ :references (\"200208121222.g7CCMdb80690@msg.id\")
+ :in-reply-to \"200208121222.g7CCMdb80690@msg.id\"
+ :message-id \"foobar32423847ef23@pluto.net\"
+ :maildir: \"/archive\"
+ :path \"/home/mickey/Maildir/inbox/cur/1312_3.32282.pluto,4cd5bd4e9:2,\"
+ :priority high
+ :flags (new unread)
+ :meta <meta-data>
+ )
+ ( .... more messages )
+)
+;; eox
+ => this will be passed to `mu4e-headers-append-func'.
+
+ 2b. After the list of headers has been returned (see 2a.),
+ we'll receive a sexp that looks like
+ (:found <n>) with n the number of messages found. The <n> will be
+ passed to `mu4e-found-func'.
+
+ 3. a view looks like:
+ (:view <msg-sexp>)
+ => the <msg-sexp> (see 2.) will be passed to `mu4e-view-func'.
+ the <msg-sexp> also contains :body-txt and/or :body-html
+
+ 4. a database update looks like:
+ (:update <msg-sexp> :move <nil-or-t>)
+ like :header
+
+ => the <msg-sexp> (see 2.) will be passed to
+ `mu4e-update-func', :move tells us whether this is a move to
+ another maildir, or merely a flag change.
+
+ 5. a remove looks like:
+ (:remove <docid>)
+ => the docid will be passed to `mu4e-remove-func'
+
+ 6. a compose looks like:
+ (:compose <reply|forward|edit|new> [:original<msg-sexp>] [:include <attach>])
+ `mu4e-compose-func'. :original looks like :view."
+ (mu4e-log 'misc "* Received %d byte(s)" (length str))
+ (setq mu4e--server-buf (concat mu4e--server-buf str)) ;; update our buffer
+ (let ((sexp (mu4e--server-eat-sexp-from-buf)))
+ (with-local-quit
+ (while sexp
+ (mu4e-log 'from-server "%s" sexp)
+ (cond
+ ;; a header plist can be recognized by the existence of a :date field
+ ((plist-get sexp :headers)
+ (funcall mu4e-headers-append-func (plist-get sexp :headers)))
+
+ ;; the found sexp, we receive after getting all the headers
+ ((plist-get sexp :found)
+ (funcall mu4e-found-func (plist-get sexp :found)))
+
+ ;; viewing a specific message
+ ((plist-get sexp :view)
+ (funcall mu4e-view-func (plist-get sexp :view)))
+
+ ;; receive an erase message
+ ((plist-get sexp :erase)
+ (funcall mu4e-erase-func))
+
+ ;; receive a :sent message
+ ((plist-get sexp :sent)
+ (funcall mu4e-sent-func
+ (plist-get sexp :docid)
+ (plist-get sexp :path)))
+
+ ;; received a pong message
+ ((plist-get sexp :pong)
+ (setq mu4e--server-props (plist-get sexp :props))
+ (funcall mu4e-pong-func sexp))
+
+ ;; received a contacts message
+ ;; note: we use 'member', to match (:contacts nil)
+ ((plist-member sexp :contacts)
+ (funcall mu4e-contacts-func
+ (plist-get sexp :contacts)
+ (plist-get sexp :tstamp)))
+
+ ;; something got moved/flags changed
+ ((plist-get sexp :update)
+ (funcall mu4e-update-func
+ (plist-get sexp :update)
+ (plist-get sexp :move)
+ (plist-get sexp :maybe-view)))
+
+ ;; a message got removed
+ ((plist-get sexp :remove)
+ (funcall mu4e-remove-func (plist-get sexp :remove)))
+
+ ;; start composing a new message
+ ((plist-get sexp :compose)
+ (funcall mu4e-compose-func
+ (plist-get sexp :compose)
+ (plist-get sexp :original)
+ (plist-get sexp :include)))
+
+ ;; get some info
+ ((plist-get sexp :info)
+ (funcall mu4e-info-func sexp))
+
+ ;; receive an error
+ ((plist-get sexp :error)
+ (funcall mu4e-error-func
+ (plist-get sexp :error)
+ (plist-get sexp :message)))
+
+ (t (mu4e-message "Unexpected data from server [%S]" sexp)))
+
+ (setq sexp (mu4e--server-eat-sexp-from-buf))))))
+
+(defun mu4e--kill-stale ()
+ "Kill stale mu4e server process.
+As per issue #2198."
+ (seq-each
+ (lambda(proc)
+ (when (and (process-live-p proc)
+ (string-prefix-p mu4e--server-name (process-name proc)))
+ (mu4e-message "killing stale mu4e server")
+ (ignore-errors
+ (signal-process proc 'SIGINT) ;; nicely
+ (sit-for 1.0)
+ (signal-process proc 'SIGKILL)))) ;; forcefully
+ (process-list)))
+
+(defun mu4e--server-start ()
+ "Start the mu server process."
+ (let ((default-directory temporary-file-directory)) ;;ensure it's local.
+ ;; sanity-check 1
+ (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
+ (mu4e-error
+ "Cannot find mu, please set `mu4e-mu-binary' to the mu executable path"))
+ ;; sanity-check 2
+ (let ((version (let ((s (shell-command-to-string
+ (concat mu4e-mu-binary " --version"))))
+ (and (string-match "version \\([.0-9]+\\)" s)
+ (match-string 1 s)))))
+ (unless (string= version mu4e-mu-version)
+ (mu4e-error
+ (concat
+ "Found mu version %s, but mu4e needs version %s"
+ "; please set `mu4e-mu-binary' "
+ "accordingly") version mu4e-mu-version)))
+ ;; kill old/stale servers, if any.
+ (mu4e--kill-stale)
+ (let* ((process-connection-type nil) ;; use a pipe
+ (args (when mu4e-mu-home `(,(format"--muhome=%s" mu4e-mu-home))))
+ (args (if mu4e-mu-debug (cons "--debug" args) args))
+ (args (cons "server" args)))
+ (setq mu4e--server-buf "")
+ (setq mu4e--server-process (apply 'start-process
+ mu4e--server-name mu4e--server-name
+ mu4e-mu-binary args))
+ ;; register a function for (:info ...) sexps
+ (unless mu4e--server-process
+ (mu4e-error "Failed to start the mu4e backend"))
+ (set-process-query-on-exit-flag mu4e--server-process nil)
+ (set-process-coding-system mu4e--server-process 'binary 'utf-8-unix)
+ (set-process-filter mu4e--server-process 'mu4e--server-filter)
+ (set-process-sentinel mu4e--server-process 'mu4e--server-sentinel))))
+
+(defun mu4e--server-kill ()
+ "Kill the mu server process."
+ (let* ((buf (get-buffer mu4e--server-name))
+ (proc (and (buffer-live-p buf) (get-buffer-process buf))))
+ (when proc
+ (mu4e-message "shutting down")
+ (set-process-filter mu4e--server-process nil)
+ (set-process-sentinel mu4e--server-process nil)
+ (let ((delete-exited-processes t))
+ (mu4e--server-call-mu '(quit)))
+ ;; try sending SIGINT (C-c) to process, so it can exit gracefully
+ (ignore-errors
+ (signal-process proc 'SIGINT))))
+ (setq
+ mu4e--server-process nil
+ mu4e--server-buf nil))
+
+;; error codes are defined in src/mu-util
+;;(defconst mu4e-xapian-empty 19 "Error code: xapian is empty/non-existent")
+
+(defun mu4e--server-sentinel (proc _msg)
+ "Function called when the server process PROC terminates with MSG."
+ (let ((status (process-status proc)) (code (process-exit-status proc)))
+ (setq mu4e--server-process nil)
+ (setq mu4e--server-buf "") ;; clear any half-received sexps
+ (cond
+ ((eq status 'signal)
+ (cond
+ ((or(eq code 9) (eq code 2)) (message nil))
+ ;;(message "the mu server process has been stopped"))
+ (t (error (format "mu server process received signal %d" code)))))
+ ((eq status 'exit)
+ (cond
+ ((eq code 0)
+ (message nil)) ;; don't do anything
+ ((eq code 19)
+ (error "Database is locked by another process"))
+ (t (error "Mu server process ended with exit code %d" code))))
+ (t
+ (error "Something bad happened to the mu server process")))))
+
+(defun mu4e--server-call-mu (form)
+ "Call the mu server with some command FORM."
+ (unless (mu4e-running-p) (mu4e--server-start))
+ (let* ((print-length nil) (print-level nil)
+ (cmd (format "%S" form)))
+ (mu4e-log 'to-server "%s" cmd)
+ (process-send-string mu4e--server-process (concat cmd "\n"))))
+
+(defun mu4e--server-add (path)
+ "Add the message at PATH to the database.
+On success, we receive `'(:info add :path <path> :docid <docid>)'
+as well as `'(:update <msg-sexp>)`'; otherwise, we receive an error."
+ (mu4e--server-call-mu `(add :path ,path)))
+
+(defun mu4e--server-compose (type decrypt &optional docid)
+ "Compose a message of TYPE, DECRYPT it and use DOCID.
+TYPE is a symbol, either `forward', `reply', `edit', `resend' or
+`new', based on an original message (ie, replying to, forwarding,
+editing, resending) with DOCID or nil for type `new'.
+
+The result is delivered to the function registered as
+`mu4e-compose-func'."
+ (mu4e--server-call-mu
+ `(compose
+ :type ,type
+ :decrypt ,(and decrypt t)
+ :docid ,docid)))
+
+(defun mu4e--server-contacts (personal after maxnum tstamp)
+ "Ask for contacts with PERSONAL AFTER MAXNUM TSTAMP.
+
+S-expression (:contacts (<list>) :tstamp \"<tstamp>\")
+is expected in response.
+
+If PERSONAL is non-nil, only get personal contacts, if AFTER is
+non-nil, get only contacts seen AFTER (the time_t value). If MAX is non-nil,
+get at most MAX contacts."
+ (mu4e--server-call-mu
+ `(contacts
+ :personal ,(and personal t)
+ :after ,(or after nil)
+ :tstamp ,(or tstamp nil)
+ :maxnum ,(or maxnum nil))))
+
+(defun mu4e--server-find (query threads sortfield sortdir maxnum skip-dups
+ include-related)
+ "Run QUERY with THREADS SORTFIELD SORTDIR MAXNUM SKIP-DUPS INCLUDE-RELATED.
+
+If THREADS is non-nil, show results in threaded fashion,
+SORTFIELD is a symbol describing the field to sort by (or nil);
+see `mu4e~headers-sortfield-choices'. If SORT is `descending',
+sort Z->A, if it's `ascending', sort A->Z. MAXNUM determines the
+maximum number of results to return, or nil for unlimited. If
+SKIP-DUPS is non-nil, show only one of duplicate messages (see
+`mu4e-headers-skip-duplicates'). If INCLUDE-RELATED is non-nil,
+include messages related to the messages matching the search
+query (see `mu4e-headers-include-related').
+
+For each result found, a function is called, depending on the
+kind of result. The variables `mu4e-error-func' contain the
+function that to be be called for, resp., a message (header)
+or an error."
+ (mu4e--server-call-mu
+ `(find
+ :query ,query
+ :threads ,(and threads t)
+ :sortfield ,sortfield
+ :descending ,(if (eq sortdir 'descending) t nil)
+ :maxnum ,maxnum
+ :skip-dups ,(and skip-dups t)
+ :include-related ,(and include-related t))))
+
+(defun mu4e--server-index (&optional cleanup lazy-check)
+ "Index messages.
+If CLEANUP is non-nil, remove messages which are in the database
+but no longer in the filesystem. If LAZY-CHECK is non-nil, only
+consider messages for which the time stamp (ctime) of the
+directory they reside in has not changed since the previous
+indexing run. This is much faster than the non-lazy check, but
+won't update messages that have change (rather than having been
+added or removed), since merely editing a message does not update
+the directory time stamp."
+ (mu4e--server-call-mu
+ `(index :cleanup ,(and cleanup t)
+ :lazy-check ,(and lazy-check t))))
+
+(defun mu4e--server-mkdir (path)
+ "Create a new maildir-directory at filesystem PATH."
+ (mu4e--server-call-mu `(mkdir :path ,path)))
+
+(defun mu4e--server-move (docid-or-msgid &optional maildir flags no-view)
+ "Move message identified by DOCID-OR-MSGID.
+Optionally to MAILDIR and optionally setting FLAGS. If MAILDIR is
+nil, message will be moved within the same maildir.
+
+At least one of MAILDIR and FLAGS must be specified. Note that
+even when MAILDIR is nil, this is still a filesystem move, since
+a change in flags implies a change in message filename.
+
+MAILDIR must be a maildir, that is, the part _without_ cur/ or new/
+or the root-maildir-prefix. E.g. \"/archive\". This directory must
+already exist.
+
+The FLAGS parameter can have the following forms:
+ 1. a list of flags such as `(passed replied seen)'
+ 2. a string containing the one-char versions of the flags, e.g. \"PRS\"
+ 3. a delta-string specifying the changes with +/- and the one-char flags,
+ e.g. \"+S-N\" to set Seen and remove New.
+
+The flags are any of `deleted', `flagged', `new', `passed', `replied' `seen' or
+`trashed', or the corresponding \"DFNPRST\" as defined in [1]. See
+`mu4e-string-to-flags' and `mu4e-flags-to-string'.
+The server reports the results for the operation through
+`mu4e-update-func'.
+
+If the variable `mu4e-change-filenames-when-moving' is
+non-nil, moving to a different maildir generates new names forq
+the target files; this helps certain tools (such as mbsync).
+
+If NO-VIEW is non-nil, do not update the view.
+
+Returns either (:update ... ) or (:error ) sexp, which are handled my
+`mu4e-update-func' and `mu4e-error-func', respectively."
+ (unless (or maildir flags)
+ (mu4e-error "At least one of maildir and flags must be specified"))
+ (unless (or (not maildir)
+ (file-exists-p (concat (mu4e-root-maildir) "/" maildir "/")))
+ (mu4e-error "Target dir does not exist"))
+ (mu4e--server-call-mu
+ `(move
+ :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
+ :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
+ :flags ,(or flags nil)
+ :maildir ,(or maildir nil)
+ :rename ,(and maildir mu4e-change-filenames-when-moving t)
+ :no-view ,(and no-view t))))
+
+(defun mu4e--server-ping (&optional queries)
+ "Sends a ping to the mu server, expecting a (:pong ...) in response.
+QUERIES is a list of queries for the number of results with
+read/unread status are returned in the pong-response."
+ (mu4e--server-call-mu `(ping :queries ,queries)))
+
+(defun mu4e--server-remove (docid)
+ "Remove message with DOCID.
+The results are reporter through either (:update ... )
+or (:error) sexp, which are handled my `mu4e-error-func',
+respectively."
+ (mu4e--server-call-mu `(remove :docid ,docid)))
+
+(defun mu4e--server-sent (path)
+ "Tell the mu server we sent a message at PATH.
+If this works, we will receive (:info add :path <path> :docid
+<docid> :fcc <path>)."
+ (mu4e--server-call-mu `(sent :path ,path)))
+
+(defun mu4e--server-view (docid-or-msgid &optional mark-as-read)
+ "View a message referred to by DOCID-OR-MSGID.
+Optionally, if MARK-AS-READ is non-nil, the backend marks the
+message as \"read\" before returning, if not already. The result
+will be delivered to the function registered as `mu4e-view-func'."
+ (mu4e--server-call-mu
+ `(view
+ :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
+ :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
+ :mark-as-read ,(and mark-as-read t)
+ ;; when moving (due to mark-as-read), change filenames
+ ;; if so configured. Note: currently this *ignored*
+ ;; because mbsync seems to get confused.
+ :rename ,(and mu4e-change-filenames-when-moving t))))
+
+\f
+(provide 'mu4e-server)
+;;; mu4e-server.el ends here
--- /dev/null
+;;; mu4e-speedbar --- Speedbar support for mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2021 Antono Vasiljev, Dirk-Jan C. Binnema
+
+;; Author: Antono Vasiljev <self@antono.info>
+;; Version: 0.1
+;; Keywords: file, tags, tools
+
+;; This file is not part of GNU Emacs.
+
+;; This program is free software; you can redistribute 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, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Speedbar provides a frame in which files, and locations in files
+;; are displayed. These functions provide mu4e specific support,
+;; showing maildir list in the side-bar.
+;;
+;; This file requires speedbar.
+
+;;; Code:
+
+(require 'speedbar)
+(require 'mu4e-vars)
+(require 'mu4e-headers)
+(require 'mu4e-context)
+(require 'mu4e-bookmarks)
+
+(defvar mu4e-main-speedbar-key-map nil
+ "Keymap used when in mu4e display mode.")
+(defvar mu4e-headers-speedbar-key-map nil
+ "Keymap used when in mu4e display mode.")
+(defvar mu4e-view-speedbar-key-map nil
+ "Keymap used when in mu4e display mode.")
+
+(defvar mu4e-main-speedbar-menu-items nil
+ "Additional menu-items to add to speedbar frame.")
+(defvar mu4e-headers-speedbar-menu-items nil
+ "Additional menu-items to add to speedbar frame.")
+(defvar mu4e-view-speedbar-menu-items nil
+ "Additional menu-items to add to speedbar frame.")
+
+
+(defun mu4e-speedbar-install-variables ()
+ "Install those variables used by speedbar to enhance mu4e."
+ (add-hook 'mu4e-context-changed-hook
+ #'mu4e~speedbar-context-changed-hook-fn)
+ (dolist (keymap
+ '( mu4e-main-speedbar-key-map
+ mu4e-headers-speedbar-key-map
+ mu4e-view-speedbar-key-map))
+ (unless keymap
+ (setq keymap (speedbar-make-specialized-keymap))
+ (define-key keymap "RET" 'speedbar-edit-line)
+ (define-key keymap "e" 'speedbar-edit-line))))
+
+(defun mu4e~speedbar-context-changed-hook-fn ()
+ (when (buffer-live-p speedbar-buffer)
+ (with-current-buffer speedbar-buffer
+ (let ((inhibit-read-only t))
+ (mu4e-speedbar-buttons)))))
+
+(with-eval-after-load 'speedbar
+ (mu4e-speedbar-install-variables))
+
+(defun mu4e~speedbar-render-maildir-list ()
+ "Insert the list of maildirs in the speedbar."
+ (interactive)
+ (when (buffer-live-p speedbar-buffer)
+ (with-current-buffer speedbar-buffer
+ (mapcar (lambda (maildir-name)
+ (speedbar-insert-button
+ (concat " " maildir-name)
+ 'mu4e-highlight-face
+ 'highlight
+ 'mu4e~speedbar-maildir
+ maildir-name))
+ (mu4e-get-maildirs)))))
+
+(defun mu4e~speedbar-maildir (&optional _text token _ident)
+ "Jump to maildir TOKEN. TEXT and INDENT are not used."
+ (dframe-with-attached-buffer
+ (mu4e-search (concat "\"maildir:" token "\"") current-prefix-arg)))
+
+(defun mu4e~speedbar-render-bookmark-list ()
+ "Insert the list of bookmarks in the speedbar"
+ (interactive)
+ (mapcar (lambda (bookmark)
+ (unless (plist-get bookmark :hide)
+ (speedbar-insert-button
+ (concat " " (plist-get bookmark :name))
+ 'mu4e-highlight-face
+ 'highlight
+ 'mu4e~speedbar-bookmark
+ (plist-get bookmark :query))))
+ (mu4e-bookmarks)))
+
+(defun mu4e~speedbar-bookmark (&optional _text token _ident)
+ "Run bookmarked query TOKEN. TEXT and INDENT are not used."
+ (dframe-with-attached-buffer
+ (mu4e-search token current-prefix-arg)))
+
+;;;###autoload
+(defun mu4e-speedbar-buttons (&optional _buffer)
+ "Create buttons for any mu4e BUFFER."
+ (interactive)
+ (erase-buffer)
+ (insert (propertize "* mu4e\n\n" 'face 'mu4e-title-face))
+
+ (insert (propertize " Bookmarks\n" 'face 'mu4e-title-face))
+ (mu4e~speedbar-render-bookmark-list)
+ (insert "\n")
+ (insert (propertize " Maildirs\n" 'face 'mu4e-title-face))
+ (mu4e~speedbar-render-maildir-list))
+
+(defun mu4e-main-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+(defun mu4e-headers-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+(defun mu4e-view-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+
+;;; _
+(provide 'mu4e-speedbar)
+;;; mu4e-speedbar.el ends here
--- /dev/null
+;;; mu4e-update.el -- part of mu4e, -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Updating the mu4e message store: calling a mail retrieval program and
+;; re-running the index.
+
+;;; Code:
+
+(require 'mu4e-helpers)
+(require 'mu4e-server)
+\f
+;;; Customization
+
+(defcustom mu4e-get-mail-command "true"
+ "Shell command for retrieving new mail.
+Common values are \"offlineimap\", \"fetchmail\" or \"mbsync\", but
+arbitrary shell-commands can be used.
+
+When set to the literal string \"true\" (the default), the
+command simply finishes successfully (running the \"true\"
+command) without retrieving any mail. This can be useful when
+mail is already retrieved in another way, such as a local MDA."
+ :type 'string
+ :group 'mu4e
+ :safe 'stringp)
+
+(defcustom mu4e-index-update-error-warning t
+ "Whether to display warnings during the retrieval process.
+This depends on the `mu4e-get-mail-command' exit code."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-index-update-error-continue t
+ "Whether to continue with indexing after an error during retrieval."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-index-update-in-background t
+ "Whether to retrieve mail in the background."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-index-cleanup t
+ "Whether to run a cleanup phase after indexing.
+
+That is, validate that each message in the message store has a
+corresponding message file in the filesystem.
+
+Having this option as t ensures that no non-existing messages are
+shown but can slow with large message stores on slow file-systems."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-index-lazy-check nil
+ "Whether to only use a \"lazy\" check during reindexing.
+This influences how we decide whether a message
+needs (re)indexing or not.
+
+When this is set to non-nil, mu only uses the directory
+timestamps to decide whether it needs to check the messages
+beneath it. This makes indexing much faster, but might miss some
+changes. For this, you might want to occasionally call
+`mu4e-update-index-nonlazy'; `mu4e-update-pre-hook' can be used
+to automate this."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-update-interval nil
+ "Number of seconds between mail retrieval/indexing.
+If nil, don't update automatically. Note, changes in
+`mu4e-update-interval' only take effect after restarting mu4e."
+ :type '(choice (const :tag "No automatic update" nil)
+ (integer :tag "Seconds"))
+ :group 'mu4e
+ :safe 'integerp)
+
+(defvar mu4e-update-pre-hook nil
+ "Hook run just *before* the mail-retrieval / database updating process starts.
+You can use this hook for example to `mu4e-get-mail-command' with
+some specific setting.")
+
+(defcustom mu4e-hide-index-messages nil
+ "Whether to hide the \"Indexing...\" and contacts messages."
+ :type 'boolean
+ :group 'mu4e)
+
+(defvar mu4e-index-updated-hook nil
+ "Hook run when the indexing process has completed.
+The variable `mu4e-index-update-status' can be used to get
+information about what changed.")
+
+(defvar mu4e-message-changed-hook nil
+ "Hook run when there is a message changed in the data store.
+For new messages, it depends on `mu4e-index-updated-hook'. This
+can be used as a simple way to invoke some action when a message
+changed")
+
+(defvar mu4e-index-update-status nil
+ "Last-seen completed update status, based on server status messages.
+
+If non-nil, this is a plist of the form:
+\(
+:checked <number of messages processed> (checked whether up-to-date)
+:updated <number of messages updated/added
+:cleaned-up <number of stale messages removed from store
+:stamp <emacs (current-time) timestamp for the status)")
+
+
+\f
+;;; Internal variables
+(defvar mu4e--progress-reporter nil
+ "Internal, the progress reporter object.")
+(defvar mu4e--update-timer nil
+ "The mu4e update timer.")
+(defconst mu4e--update-name " *mu4e-update*"
+ "Name of the process and buffer to update mail.")
+(defconst mu4e--update-buffer-height 8
+ "Height of the mu4e message retrieval/update buffer.")
+(defvar mu4e--get-mail-ask-password "mu4e get-mail: Enter password: "
+ "Query string for `mu4e-get-mail-command' password.")
+(defvar mu4e--get-mail-password-regexp "^Remote: Enter password: $"
+ "Regexp for a `mu4e-get-mail-command' password query.")
+\f
+
+(defun mu4e--get-mail-process-filter (proc msg)
+ "Filter the MSG output of the `mu4e-get-mail-command' PROC.
+
+Currently the filter only checks if the command asks for a
+password by matching the output against
+`mu4e~get-mail-password-regexp'. The messages are inserted into
+the process buffer.
+
+Also scrolls to the final line, and update the progress
+throbber."
+ (when mu4e--progress-reporter
+ (progress-reporter-update mu4e--progress-reporter))
+
+ (when (string-match mu4e--get-mail-password-regexp msg)
+ (if (process-get proc 'x-interactive)
+ (process-send-string proc
+ (concat (read-passwd mu4e--get-mail-ask-password)
+ "\n"))
+ ;; TODO kill process?
+ (mu4e-error "Unrecognized password request")))
+ (when (process-buffer proc)
+ (let ((inhibit-read-only t)
+ (procwin (get-buffer-window (process-buffer proc))))
+ ;; Insert at end of buffer. Leave point alone.
+ (with-current-buffer (process-buffer proc)
+ (goto-char (point-max))
+ (if (string-match ".*\r\\(.*\\)" msg)
+ (progn
+ ;; kill even with \r
+ (end-of-line)
+ (let ((end (point)))
+ (beginning-of-line)
+ (delete-region (point) end))
+ (insert (match-string 1 msg)))
+ (insert msg)))
+ ;; Auto-scroll unless user is interacting with the window.
+ (when (and (window-live-p procwin)
+ (not (eq (selected-window) procwin)))
+ (with-selected-window procwin
+ (goto-char (point-max)))))))
+
+(defun mu4e-index-message (frm &rest args)
+ "Display FRM with ARGS like `mu4e-message' for index messages.
+However, if `mu4e-hide-index-messages' is non-nil, do not display anything."
+ (unless mu4e-hide-index-messages
+ (apply 'mu4e-message frm args)))
+
+(defun mu4e-update-index ()
+ "Update the mu4e index."
+ (interactive)
+ (mu4e--server-index mu4e-index-cleanup mu4e-index-lazy-check))
+
+(defun mu4e-update-index-nonlazy ()
+ "Update the mu4e index non-lazily.
+This is just a convenience wrapper for indexing the non-lazy way
+if you otherwise want to use `mu4e-index-lazy-check'."
+ (interactive)
+ (let ((mu4e-index-cleanup t) (mu4e-index-lazy-check nil))
+ (mu4e-update-index)))
+
+(defvar mu4e--update-buffer nil
+ "The buffer of the update process when updating.")
+
+(define-derived-mode mu4e--update-mail-mode
+special-mode "mu4e:update"
+ "Major mode used for retrieving new e-mail messages in `mu4e'.")
+
+(define-key mu4e--update-mail-mode-map (kbd "q") 'mu4e-kill-update-mail)
+
+(defun mu4e--temp-window (buf height)
+ "Create a temporary window with HEIGHT at the bottom BUF."
+ (let ((win
+ (split-window
+ (frame-root-window)
+ (- (window-height (frame-root-window)) height))))
+ (set-window-buffer win buf)
+ (set-window-dedicated-p win t)
+ win))
+
+(defun mu4e--update-sentinel-func (proc _msg)
+ "Sentinel function for the update process PROC."
+ (when mu4e--progress-reporter
+ (progress-reporter-done mu4e--progress-reporter)
+ (setq mu4e--progress-reporter nil))
+ (unless mu4e-hide-index-messages
+ (message nil))
+ (if (or (not (eq (process-status proc) 'exit))
+ (/= (process-exit-status proc) 0))
+ (progn
+ (when mu4e-index-update-error-warning
+ (mu4e-message "Update process returned with non-zero exit code")
+ (sit-for 5))
+ (when mu4e-index-update-error-continue
+ (mu4e-update-index)))
+ (mu4e-update-index))
+ (when (buffer-live-p mu4e--update-buffer)
+ (unless (eq mu4e-split-view 'single-window)
+ (mapc #'delete-window (get-buffer-window-list mu4e--update-buffer)))
+ (kill-buffer mu4e--update-buffer)))
+
+;; complicated function, as it:
+;; - needs to check for errors
+;; - (optionally) pop-up a window
+;; - (optionally) check password requests
+(defun mu4e--update-mail-and-index-real (run-in-background)
+ "Get a new mail by running `mu4e-get-mail-command'.
+If
+RUN-IN-BACKGROUND is non-nil (or called with prefix-argument),
+run in the background; otherwise, pop up a window."
+ (let* ((process-connection-type t)
+ (proc (start-process-shell-command
+ "mu4e-update" mu4e--update-name
+ mu4e-get-mail-command))
+ (buf (process-buffer proc))
+ (win (or run-in-background
+ (mu4e--temp-window buf mu4e--update-buffer-height))))
+ (setq mu4e--update-buffer buf)
+ (when (window-live-p win)
+ (with-selected-window win
+ ;; ;;(switch-to-buffer buf)
+ ;; (set-window-dedicated-p win t)
+ (erase-buffer)
+ (insert "\n") ;; FIXME -- needed so output starts
+ (mu4e--update-mail-mode)))
+ (setq mu4e--progress-reporter
+ (unless mu4e-hide-index-messages
+ (make-progress-reporter
+ (mu4e-format "Retrieving mail..."))))
+ (set-process-sentinel proc 'mu4e--update-sentinel-func)
+ ;; if we're running in the foreground, handle password requests
+ (unless run-in-background
+ (process-put proc 'x-interactive (not run-in-background))
+ (set-process-filter proc 'mu4e--get-mail-process-filter))))
+
+(defun mu4e-update-mail-and-index (run-in-background)
+ "Retrieve new mail by running `mu4e-get-mail-command'.
+If RUN-IN-BACKGROUND is non-nil (or called with prefix-argument),
+run in the background; otherwise, pop up a window."
+ (interactive "P")
+ (unless mu4e-get-mail-command
+ (mu4e-error "`mu4e-get-mail-command' is not defined"))
+ (if (and (buffer-live-p mu4e--update-buffer)
+ (process-live-p (get-buffer-process mu4e--update-buffer)))
+ (mu4e-message "Update process is already running")
+ (progn
+ (run-hooks 'mu4e-update-pre-hook)
+ (mu4e--update-mail-and-index-real run-in-background))))
+
+(defun mu4e-kill-update-mail ()
+ "Stop the update process by killing it."
+ (interactive)
+ (let* ((proc (and (buffer-live-p mu4e--update-buffer)
+ (get-buffer-process mu4e--update-buffer))))
+ (when (process-live-p proc)
+ (kill-process proc t))))
+
+(define-obsolete-function-alias 'mu4e-interrupt-update-mail
+ 'mu4e-kill-update-mail "1.0-alpha0")
+
+(define-minor-mode mu4e-update-minor-mode
+ "Mode for triggering mu4e updates."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ :keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index)
+ ;; for terminal users
+ (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+ map))
+
+(provide 'mu4e-update)
+;;; mu4e-update.el ends here
--- /dev/null
+;;; mu4e-vars.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'message)
+(require 'mu4e-helpers)
+\f
+;;; Configuration
+(defgroup mu4e nil
+ "Mu4e - an email-client for Emacs."
+ :group 'mail)
+
+(defcustom mu4e-headers-include-related t
+ "Wether to include \"related\" messages in queries.
+With this option set to non-nil, not just return the matches for
+a searches, but also messages that are related (through their
+references) to these messages. This can be useful e.g. to include
+sent messages into message threads."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-skip-duplicates t
+ "Whether to skip duplicate messages.
+With this option set to non-nil, show only one of duplicate
+messages. This is useful when you have multiple copies of the same
+message, which is a common occurrence for example when using Gmail
+and offlineimap."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+(defcustom mu4e-date-format-long "%c"
+ "Date format to use in the message view.
+Follows the format of `format-time-string'."
+ :type 'string
+ :group 'mu4e)
+
+\f
+;;; Faces
+
+(defgroup mu4e-faces nil
+ "Type faces (fonts) used in mu4e."
+ :group 'mu4e
+ :group 'faces)
+
+(defface mu4e-unread-face
+ '((t :inherit font-lock-keyword-face :weight bold))
+ "Face for an unread message header."
+ :group 'mu4e-faces)
+
+(defface mu4e-trashed-face
+ '((t :inherit font-lock-comment-face :strike-through t))
+ "Face for an message header in the trash folder."
+ :group 'mu4e-faces)
+
+(defface mu4e-draft-face
+ '((t :inherit font-lock-string-face))
+ "Face for a draft message header.
+I.e. a message with the draft flag set."
+ :group 'mu4e-faces)
+
+(defface mu4e-flagged-face
+ '((t :inherit font-lock-constant-face :weight bold))
+ "Face for a flagged message header."
+ :group 'mu4e-faces)
+
+(defface mu4e-replied-face
+ '((t :inherit font-lock-builtin-face :weight normal :slant normal))
+ "Face for a replied message header."
+ :group 'mu4e-faces)
+
+(defface mu4e-forwarded-face
+ '((t :inherit font-lock-builtin-face :weight normal :slant normal))
+ "Face for a passed (forwarded) message header."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-face
+ '((t :inherit default))
+ "Face for a header without any special flags."
+ :group 'mu4e-faces)
+
+(defface mu4e-related-face
+ '((t :inherit default :slant italic))
+ "Face for a \='related' header." :group 'mu4e-faces)
+
+(defface mu4e-header-title-face
+ '((t :inherit font-lock-type-face))
+ "Face for a header title in the headers view."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-highlight-face
+ `((t :inherit hl-line :weight bold :underline t
+ ,@(and (>= emacs-major-version 27) '(:extend t))))
+ "Face for the header at point."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-marks-face
+ '((t :inherit font-lock-preprocessor-face))
+ "Face for the mark in the headers list."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-key-face
+ '((t :inherit message-header-name :weight bold))
+ "Face for a header key (such as \"Foo\" in \"Subject:\ Foo\")."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-value-face
+ '((t :inherit font-lock-type-face))
+ "Face for a header value (such as \"Re: Hello!\")."
+ :group 'mu4e-faces)
+
+(defface mu4e-special-header-value-face
+ '((t :inherit font-lock-builtin-face))
+ "Face for special header values."
+ :group 'mu4e-faces)
+
+(defface mu4e-link-face
+ '((t :inherit link))
+ "Face for showing URLs and attachments in the message view."
+ :group 'mu4e-faces)
+
+(defface mu4e-contact-face
+ '((t :inherit font-lock-variable-name-face))
+ "Face for showing URLs and attachments in the message view."
+ :group 'mu4e-faces)
+
+(defface mu4e-highlight-face
+ '((t :inherit highlight))
+ "Face for highlighting things."
+ :group 'mu4e-faces)
+
+(defface mu4e-title-face
+ '((t :inherit font-lock-type-face :weight bold))
+ "Face for a header title in the headers view."
+ :group 'mu4e-faces)
+
+(defface mu4e-modeline-face
+ '((t :inherit font-lock-string-face :weight bold))
+ "Face for the query in the mode-line."
+ :group 'mu4e-faces)
+
+(defface mu4e-footer-face
+ '((t :inherit font-lock-comment-face))
+ "Face for message footers (signatures)."
+ :group 'mu4e-faces)
+
+(defface mu4e-url-number-face
+ '((t :inherit font-lock-constant-face :weight bold))
+ "Face for the number tags for URLs."
+ :group 'mu4e-faces)
+
+(defface mu4e-system-face
+ '((t :inherit font-lock-comment-face :slant italic))
+ "Face for system message (such as the footers for message headers)."
+ :group 'mu4e-faces)
+
+(defface mu4e-ok-face
+ '((t :inherit font-lock-comment-face :weight bold :slant normal))
+ "Face for things that are okay."
+ :group 'mu4e-faces)
+
+(defface mu4e-warning-face
+ '((t :inherit font-lock-warning-face :weight bold :slant normal))
+ "Face for warnings / error."
+ :group 'mu4e-faces)
+
+(defface mu4e-compose-separator-face
+ '((t :inherit message-separator :slant italic))
+ "Face for the headers/message separator in mu4e-compose-mode."
+ :group 'mu4e-faces)
+
+(defface mu4e-region-code
+ '((t (:background "DarkSlateGray")))
+ "Face for highlighting marked region in mu4e-view buffer."
+ :group 'mu4e-faces)
+
+;;; Header information
+
+(defconst mu4e-header-info
+ '((:bcc
+ . (:name "Bcc"
+ :shortname "Bcc"
+ :help "Blind Carbon-Copy recipients for the message"
+ :sortable t))
+ (:cc
+ . (:name "Cc"
+ :shortname "Cc"
+ :help "Carbon-Copy recipients for the message"
+ :sortable t))
+ (:changed
+ . (:name "Changed"
+ :shortname "Chg"
+ :help "Date/time when the message was changed most recently"
+ :sortable t))
+ (:date
+ . (:name "Date"
+ :shortname "Date"
+ :help "Date/time when the message was sent"
+ :sortable t))
+ (:human-date
+ . (:name "Date"
+ :shortname "Date"
+ :help "Date/time when the message was sent"
+ :sortable :date))
+ (:flags
+ . (:name "Flags"
+ :shortname "Flgs"
+ :help "Flags for the message"
+ :sortable nil))
+ (:from
+ . (:name "From"
+ :shortname "From"
+ :help "The sender of the message"
+ :sortable t))
+ (:from-or-to
+ . (:name "From/To"
+ :shortname "From/To"
+ :help "Sender of the message if it's not me; otherwise the recipient"
+ :sortable nil))
+ (:maildir
+ . (:name "Maildir"
+ :shortname "Maildir"
+ :help "Maildir for this message"
+ :sortable t))
+ (:list
+ . (:name "List-Id"
+ :shortname "List"
+ :help "Mailing list id for this message"
+ :sortable t))
+ (:mailing-list
+ . (:name "List"
+ :shortname "List"
+ :help "Mailing list friendly name for this message"
+ :sortable :list))
+ (:message-id
+ . (:name "Message-Id"
+ :shortname "MsgID"
+ :help "Message-Id for this message"
+ :sortable nil))
+ (:path
+ . (:name "Path"
+ :shortname "Path"
+ :help "Full filesystem path to the message"
+ :sortable t))
+ (:size
+ . (:name "Size"
+ :shortname "Size"
+ :help "Size of the message"
+ :sortable t))
+ (:subject
+ . (:name "Subject"
+ :shortname "Subject"
+ :help "Subject of the message"
+ :sortable t))
+ (:tags
+ . (:name "Tags"
+ :shortname "Tags"
+ :help "Tags for the message"
+ :sortable nil))
+ (:thread-subject
+ . (:name "Subject"
+ :shortname "Subject"
+ :help "Subject of the thread"
+ :sortable :subject))
+ (:to
+ . (:name "To"
+ :shortname "To"
+ :help "Recipient of the message"
+ :sortable t)))
+ "An alist of all possible header fields and information about them.
+This is used in the user-interface (the column headers in the header list, and
+the fields the message view).
+
+Most fields should be self-explanatory. A special one is
+`:from-or-to', which is equal to `:from' unless `:from' matches
+one of the addresses in `(mu4e-personal-addresses)', in which
+case it will be equal to `:to'.
+
+Furthermore, the property `:sortable' determines whether we can
+sort by this field. This can be either a boolean (nil or t), or a
+symbol for /another/ field. For example, the `:human-date' field
+uses `:date' for that.
+
+Note, `:sortable' is not supported for custom header fields.")
+
+(defvar mu4e-header-info-custom
+ '(
+ ;; some examples & debug helpers.
+
+ (:thread-path
+ . ;; Shows the internal thread-path
+ ( :name "Thread-path"
+ :shortname "Thp"
+ :help "The thread-path"
+ :function (lambda (msg)
+ (let ((thread (mu4e-message-field msg :thread)))
+ (or (and thread (plist-get thread :path)) "")))))
+
+ (:thread-date
+ . ;; Shows the internal thread-date
+ ( :name "Thread-date"
+ :shortname "Thd"
+ :help "The thread-date"
+ :function (lambda (msg)
+ (let* ((thread (mu4e-message-field msg :thread))
+ (tdate (and thread (plist-get thread :date-tstamp))))
+ (format-time-string "%F %T " (or tdate 0))))))
+ (:recipnum
+ .
+ ( :name "Number of recipients"
+ :shortname "Recip#"
+ :help "Number of recipients for this message"
+ :function
+ (lambda (msg)
+ (format "%d"
+ (+ (length (mu4e-message-field msg :to))
+ (length (mu4e-message-field msg :cc))))))))
+
+ "A list of custom (user-defined) headers.
+The format is similar to `mu4e-header-info', but adds a :function
+property, which should point to a function that takes a message
+plist as argument, and returns a string. See the default value of
+`mu4e-header-info-custom for an example.
+
+Note that when using the gnus-based view, you only have access to
+a limited set of message fields: only the ones used in the
+header-view, not including, for instance, the message body.")
+
+;;; Internals
+
+(defvar mu4e~headers-view-win nil
+ "The view window connected to this headers view.")
+
+;; It's useful to have the current view message available to
+;; `mu4e-view-mode-hooks' functions, and we set up this variable
+;; before calling `mu4e-view-mode'. However, changing the major mode
+;; clobbers any local variables. Work around that by declaring the
+;; variable permanent-local.
+(defvar mu4e~view-message nil "The message being viewed in view mode.")
+(put 'mu4e~view-message 'permanent-local t)
+;;; _
+(provide 'mu4e-vars)
+;;; mu4e-vars.el ends here
--- /dev/null
+;;; mu4e-view.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file we define mu4e-view-mode (+ helper functions), which is used for
+;; viewing e-mail messages
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'calendar)
+(require 'gnus-art)
+(require 'comint)
+(require 'browse-url)
+(require 'button)
+(require 'epa)
+(require 'epg)
+(require 'thingatpt)
+
+(require 'mu4e-actions)
+(require 'mu4e-compose)
+(require 'mu4e-context)
+(require 'mu4e-headers)
+(require 'mu4e-mark)
+(require 'mu4e-message)
+(require 'mu4e-server)
+(require 'mu4e-search)
+ ;; utility functions
+(require 'mu4e-contacts)
+(require 'mu4e-vars)
+
+;;; Options
+
+(defcustom mu4e-view-scroll-to-next t
+ "Move to the next message with `mu4e-view-scroll-up-or-next'.
+When at the end of a message, move to the next one, if any.
+Otherwise, don't move to the next message."
+ :type 'boolean
+ :group 'mu4e-view)
+
+(defcustom mu4e-view-fields
+ '(:from :to :cc :subject :flags :date :maildir :mailing-list :tags
+ :attachments :signature :decryption)
+ "Header fields to display in the message view buffer.
+For the complete list of available headers, see
+`mu4e-header-info'.
+
+Note, when using the gnus-based viewer you can only use this add
+fields that are otherwise not shows; you can further tweak the
+fields using e.g. `gnus-article-hide-boring-headers',
+`gnus-article-hide-headers' etc., see the gnus documentation for
+details."
+ :type (list 'symbol)
+ :group 'mu4e-view)
+
+(defcustom mu4e-view-actions
+ (seq-filter 'identity
+ `( ("capture message" . mu4e-action-capture-message)
+ ("view in browser" . mu4e-action-view-in-browser)
+ ,(when (fboundp 'xwidget-webkit-browse-url)
+ '("xview in xwidget" . mu4e-action-view-in-xwidget))
+ ("show this thread" . mu4e-action-show-thread)))
+ "List of actions to perform on messages in view mode.
+The actions are cons-cells of the form:
+ (NAME . FUNC)
+where:
+* NAME is the name of the action (e.g. \"Count lines\")
+* FUNC is a function which receives a message plist as an argument.
+
+The first letter of NAME is used as a shortcut character."
+ :group 'mu4e-view
+ :type '(alist :key-type string :value-type function))
+
+(defcustom mu4e-view-open-program
+ (pcase system-type
+ ('darwin "open")
+ ('cygwin "cygstart")
+ (_ "xdg-open"))
+ "Tool to open the correct program for a given file.
+May also be a function of a single argument, the file to be
+opened.
+
+In the function-valued case a likely candidate is
+`mailcap-view-file' although note that there was an Emacs bug up
+to Emacs 29 which prevented opening a file if `mailcap-mime-data'
+specified a function as viewer."
+ :type '(choice string function)
+ :group 'mu4e-view)
+
+(defcustom mu4e-view-max-specpdl-size 4096
+ "The value of `max-specpdl-size' for displaying messages with Gnus."
+ :type 'integer
+ :group 'mu4e-view)
+
+
+
+\f
+;;; Old options
+
+;; Options from the old message view.
+(make-obsolete-variable 'mu4e-view-show-addresses
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-wrap-lines nil "0.9.9-dev7")
+(make-obsolete-variable 'mu4e-view-hide-cited nil "0.9.9-dev7")
+(make-obsolete-variable 'mu4e-view-date-format
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-image-max-width
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-image-max-height
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-save-multiple-attachments-without-asking
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachment-assoc
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachment-actions
+ "See mu4e-view-mime-part-actions" "1.7.0")
+(make-obsolete-variable 'mu4e-view-header-field-keymap
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-header-field-keymap
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-contacts-header-keymap
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachments-header-keymap
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-imagemagick-identify nil "1.7.0")
+(make-obsolete-variable 'mu4e-view-show-images
+ "No longer used" "1.7.0")
+(make-obsolete-variable 'mu4e-view-gnus "Old view is gone" "1.7.0")
+(make-obsolete-variable 'mu4e-view-use-gnus "Gnus view is the default" "1.5.10")
+
+(make-obsolete-variable 'mu4e-cited-regexp "No longer used" "1.7.0")
+
+\f
+
+;; Helpers
+
+(defun mu4e~view-quit-buffer ()
+ "Quit the mu4e-view buffer.
+This is a rather complex function, to ensure we don't disturb
+other windows."
+ (interactive)
+ (if (eq mu4e-split-view 'single-window)
+ (when (buffer-live-p (mu4e-get-view-buffer))
+ (kill-buffer (mu4e-get-view-buffer)))
+ (unless (eq major-mode 'mu4e-view-mode)
+ (mu4e-error "Must be in mu4e-view-mode (%S)" major-mode))
+ (let ((curbuf (current-buffer))
+ (curwin (selected-window))
+ (headers-win))
+ (walk-windows
+ (lambda (win)
+ ;; check whether the headers buffer window is visible
+ (when (eq (mu4e-get-headers-buffer) (window-buffer win))
+ (setq headers-win win))
+ ;; and kill any _other_ (non-selected) window that shows the current
+ ;; buffer
+ (when
+ (and
+ (eq curbuf (window-buffer win)) ;; does win show curbuf?
+ (not (eq curwin win)) ;; but it's not the curwin?
+ (not (one-window-p))) ;; and not the last one on the frame?
+ (delete-window win)))) ;; delete it!
+ ;; now, all *other* windows should be gone.
+ ;; if the headers view is also visible, kill ourselves + window; otherwise
+ ;; switch to the headers view
+ (if (window-live-p headers-win)
+ ;; headers are visible
+ (progn
+ (kill-buffer-and-window) ;; kill the view win
+ (setq mu4e~headers-view-win nil)
+ (select-window headers-win)) ;; and switch to the headers win...
+ ;; headers are not visible...
+ (progn
+ (kill-buffer)
+ (setq mu4e~headers-view-win nil)
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (switch-to-buffer (mu4e-get-headers-buffer))))))))
+
+
+(defconst mu4e~view-raw-buffer-name " *mu4e-raw-view*"
+ "Name for the raw message view buffer.")
+
+(defun mu4e-view-raw-message ()
+ "Display the raw contents of message at point in a new buffer."
+ (interactive)
+ (let ((path (mu4e-message-readable-path))
+ (buf (get-buffer-create mu4e~view-raw-buffer-name)))
+ (with-current-buffer buf
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert-file-contents path)
+ (view-mode)
+ (goto-char (point-min))))
+ (switch-to-buffer buf)))
+
+(defun mu4e-view-pipe (cmd)
+ "Pipe the message at point through shell command CMD.
+Then, display the results."
+ (interactive "sShell command: ")
+ (let ((path (mu4e-message-readable-path)))
+ (mu4e-process-file-through-pipe path cmd)))
+
+(defmacro mu4e~view-in-headers-context (&rest body)
+ "Evaluate BODY in the context of the headers buffer."
+ `(progn
+ (unless (buffer-live-p (mu4e-get-headers-buffer))
+ (mu4e-error "No headers buffer connected"))
+ (let* ((msg (mu4e-message-at-point))
+ (docid (mu4e-message-field msg :docid)))
+ (unless docid
+ (mu4e-error "Message without docid: action is not possible"))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (unless (eq mu4e-split-view 'single-window)
+ (when (get-buffer-window)
+ (select-window (get-buffer-window))))
+ (if (mu4e~headers-goto-docid docid)
+ ,@body
+ (mu4e-error "Cannot find message in headers buffer"))))))
+
+(defun mu4e-view-headers-next (&optional n)
+ "Move point to the next message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth next
+header."
+ (interactive "P")
+ (mu4e~view-in-headers-context
+ (mu4e~headers-move (or n 1))))
+
+(defun mu4e-view-headers-prev (&optional n)
+ "Move point to the previous message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth
+previous header."
+ (interactive "P")
+ (mu4e~view-in-headers-context
+ (mu4e~headers-move (- (or n 1)))))
+
+(defun mu4e~view-prev-or-next-unread (backwards)
+ "Move point to the next or previous message.
+Go to the previous message if BACKWARDS is non-nil.
+unread message header in the headers buffer connected with this
+message view. If this succeeds, return the new docid. Otherwise,
+return nil."
+ (mu4e~view-in-headers-context
+ (mu4e~headers-prev-or-next-unread backwards))
+ (if (eq mu4e-split-view 'single-window)
+ (when (eq (window-buffer) (mu4e-get-view-buffer))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (mu4e-headers-view-message)))
+ (mu4e-select-other-view)
+ (mu4e-headers-view-message)))
+
+(defun mu4e-view-headers-prev-unread ()
+ "Move point to the previous unread message header.
+If this succeeds, return the new docid. Otherwise, return nil."
+ (interactive)
+ (mu4e~view-prev-or-next-unread t))
+
+(defun mu4e-view-headers-next-unread ()
+ "Move point to the next unread message header.
+If this succeeds, return the new docid. Otherwise, return nil."
+ (interactive)
+ (mu4e~view-prev-or-next-unread nil))
+
+\f
+;;; Interactive functions
+(defun mu4e-view-action (&optional msg)
+ "Ask user for some action to apply on MSG, then do it.
+If MSG is nil apply action to message returned
+bymessage-at-point. The actions are specified in
+`mu4e-view-actions'."
+ (interactive)
+ (let* ((msg (or msg (mu4e-message-at-point)))
+ (actionfunc (mu4e-read-option "Action: " mu4e-view-actions)))
+ (funcall actionfunc msg)))
+
+(defun mu4e-view-mark-pattern ()
+ "Mark messages that match a certain pattern.
+Ask user for a kind of mark, (move, delete etc.), a field to
+match and a regular expression to match with. Then, mark all
+matching messages with that mark."
+ (interactive)
+ (mu4e~view-in-headers-context (mu4e-headers-mark-pattern)))
+
+(defun mu4e-view-mark-thread (&optional markpair)
+ "Mark whole thread with a certain mark.
+Ask user for a kind of mark (move, delete etc.), and apply it
+to all messages in the thread at point in the headers view. The
+optional MARKPAIR can also be used to provide the mark
+selection."
+ (interactive)
+ (mu4e~view-in-headers-context
+ (if markpair (mu4e-headers-mark-thread nil markpair)
+ (call-interactively 'mu4e-headers-mark-thread))))
+
+(defun mu4e-view-mark-subthread (&optional markpair)
+ "Mark subthread with a certain mark.
+Ask user for a kind of mark (move, delete etc.), and apply it
+to all messages in the subthread at point in the headers view.
+The optional MARKPAIR can also be used to provide the mark
+selection."
+ (interactive)
+ (mu4e~view-in-headers-context
+ (if markpair (mu4e-headers-mark-subthread markpair)
+ (mu4e-headers-mark-subthread))))
+
+(defun mu4e-view-search-narrow ()
+ "Run `mu4e-headers-search-narrow' in the headers buffer."
+ (interactive)
+ (mu4e~view-in-headers-context (mu4e-search-narrow)))
+
+(defun mu4e-view-search-edit ()
+ "Run `mu4e-search-edit' in the headers buffer."
+ (interactive)
+ (mu4e~view-in-headers-context (mu4e-search-edit)))
+
+(defun mu4e-mark-region-code ()
+ "Highlight region marked with `message-mark-inserted-region'.
+Add this function to `mu4e-view-mode-hook' to enable this feature."
+ (require 'message)
+ (let (beg end ov-beg ov-end ov-inv)
+ (save-excursion
+ (goto-char (point-min))
+ (while (re-search-forward
+ (concat "^" message-mark-insert-begin) nil t)
+ (setq ov-beg (match-beginning 0)
+ ov-end (match-end 0)
+ ov-inv (make-overlay ov-beg ov-end)
+ beg ov-end)
+ (overlay-put ov-inv 'invisible t)
+ (overlay-put ov-inv 'mu4e-overlay t)
+ (when (re-search-forward
+ (concat "^" message-mark-insert-end) nil t)
+ (setq ov-beg (match-beginning 0)
+ ov-end (match-end 0)
+ ov-inv (make-overlay ov-beg ov-end)
+ end ov-beg)
+ (overlay-put ov-inv 'invisible t))
+ (when (and beg end)
+ (let ((ov (make-overlay beg end)))
+ (overlay-put ov 'mu4e-overlay t)
+ (overlay-put ov 'face 'mu4e-region-code))
+ (setq beg nil end nil))))))
+
+;;; View Utilities
+
+(defun mu4e-view-mark-custom ()
+ "Run some custom mark function."
+ (mu4e~view-in-headers-context
+ (mu4e-headers-mark-custom)))
+
+(defun mu4e~view-split-view-p ()
+ "Return t if we're in split-view, nil otherwise."
+ (member mu4e-split-view '(horizontal vertical)))
+
+;;; Scroll commands
+
+(defun mu4e-view-scroll-up-or-next ()
+ "Scroll-up the current message.
+If `mu4e-view-scroll-to-next' is non-nil, and we cannot scroll up
+any further, go the next message."
+ (interactive)
+ (condition-case nil
+ (scroll-up)
+ (error
+ (when mu4e-view-scroll-to-next
+ (mu4e-view-headers-next)))))
+
+(defun mu4e-scroll-up ()
+ "Scroll text of selected window up one line."
+ (interactive)
+ (scroll-up 1))
+
+(defun mu4e-scroll-down ()
+ "Scroll text of selected window down one line."
+ (interactive)
+ (scroll-down 1))
+
+;;; Mark commands
+
+(defun mu4e-view-unmark-all ()
+ "If we're in split-view, unmark all messages.
+Otherwise, warn user that unmarking only works in the header
+list."
+ (interactive)
+ (if (mu4e~view-split-view-p)
+ (mu4e~view-in-headers-context (mu4e-mark-unmark-all))
+ (mu4e-message "Unmarking needs to be done in the header list view")))
+
+(defun mu4e-view-unmark ()
+ "If we're in split-view, unmark message at point.
+Otherwise, warn user that unmarking only works in the header
+list."
+ (interactive)
+ (if (mu4e~view-split-view-p)
+ (mu4e-view-mark-for-unmark)
+ (mu4e-message "Unmarking needs to be done in the header list view")))
+
+(defmacro mu4e~view-defun-mark-for (mark)
+ "Define a function mu4e-view-mark-for- MARK."
+ (let ((funcname (intern (format "mu4e-view-mark-for-%s" mark)))
+ (docstring (format "Mark the current message for %s." mark)))
+ `(progn
+ (defun ,funcname () ,docstring
+ (interactive)
+ (mu4e~view-in-headers-context
+ (mu4e-headers-mark-and-next ',mark)))
+ (put ',funcname 'definition-name ',mark))))
+
+(mu4e~view-defun-mark-for move)
+(mu4e~view-defun-mark-for refile)
+(mu4e~view-defun-mark-for delete)
+(mu4e~view-defun-mark-for flag)
+(mu4e~view-defun-mark-for unflag)
+(mu4e~view-defun-mark-for unmark)
+(mu4e~view-defun-mark-for something)
+(mu4e~view-defun-mark-for read)
+(mu4e~view-defun-mark-for unread)
+(mu4e~view-defun-mark-for trash)
+(mu4e~view-defun-mark-for untrash)
+
+(defun mu4e-view-marked-execute ()
+ "Execute the marked actions."
+ (interactive)
+ (mu4e~view-in-headers-context
+ (mu4e-mark-execute-all)))
+
+
+;;; URL handling
+
+(defvar mu4e~view-link-map nil
+ "A map of some number->url so we can jump to url by number.")
+(put 'mu4e~view-link-map 'permanent-local t)
+
+(defvar mu4e-view-active-urls-keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map [down-mouse-1] 'mu4e~view-browse-url-from-binding)
+ (define-key map [mouse-1] 'mu4e~view-browse-url-from-binding)
+ (define-key map (kbd "M-<return>") 'mu4e~view-browse-url-from-binding)
+ map)
+ "Keymap used for the urls inside the body.")
+
+(defvar mu4e~view-beginning-of-url-regexp
+ "https?\\://\\|mailto:"
+ "Regexp that matches the beginning of certain URLs.
+Match-string 1 will contain the matched URL, if any.")
+
+
+(defun mu4e~view-browse-url-from-binding (&optional url)
+ "View in browser the url at point, or click location.
+If the optional argument URL is provided, browse that instead.
+If the url is mailto link, start writing an email to that address."
+ (interactive)
+ (let* (( url (or url (mu4e~view-get-property-from-event 'mu4e-url))))
+ (when url
+ (if (string-match-p "^mailto:" url)
+ (browse-url-mail url)
+ (browse-url url)))))
+
+
+(defun mu4e~view-get-property-from-event (prop)
+ "Get the property PROP at point, or the location of the mouse.
+The action is chosen based on the `last-command-event'.
+Meant to be evoked from interactive commands."
+ (if (and (eventp last-command-event)
+ (mouse-event-p last-command-event))
+ (let ((posn (event-end last-command-event)))
+ (when (numberp (posn-point posn))
+ (get-text-property
+ (posn-point posn)
+ prop
+ (window-buffer (posn-window posn)))))
+ (get-text-property (point) prop)))
+
+;; this is fairly simplistic...
+(defun mu4e~view-activate-urls ()
+ "Turn things that look like URLs into clickable things.
+Also number them so they can be opened using `mu4e-view-go-to-url'."
+ (let ((num 0))
+ (save-excursion
+ (setq mu4e~view-link-map ;; buffer local
+ (make-hash-table :size 32 :weakness nil))
+ (goto-char (point-min))
+ (while (re-search-forward mu4e~view-beginning-of-url-regexp nil t)
+ (let ((bounds (thing-at-point-bounds-of-url-at-point)))
+ (when bounds
+ (let* ((url (thing-at-point-url-at-point))
+ (ov (make-overlay (car bounds) (cdr bounds))))
+ (puthash (cl-incf num) url mu4e~view-link-map)
+ (add-text-properties
+ (car bounds)
+ (cdr bounds)
+ `(face mu4e-link-face
+ mouse-face highlight
+ mu4e-url ,url
+ keymap ,mu4e-view-active-urls-keymap
+ help-echo
+ "[mouse-1] or [M-RET] to open the link"))
+ (overlay-put ov 'mu4e-overlay t)
+ (overlay-put ov 'after-string
+ (propertize (format "\u200B[%d]" num)
+ 'face 'mu4e-url-number-face)))))))))
+
+
+(defun mu4e~view-get-urls-num (prompt &optional multi)
+ "Ask the user with PROMPT for an URL number for MSG.
+The number is [1..n] for URLs \[0..(n-1)] in the message. If
+MULTI is nil, return the number for the URL; otherwise (MULTI is
+non-nil), accept ranges of URL numbers, as per
+`mu4e-split-ranges-to-numbers', and return the corresponding
+string."
+ (let* ((count (hash-table-count mu4e~view-link-map)) (def))
+ (when (zerop count) (mu4e-error "No links for this message"))
+ (if (not multi)
+ (if (= count 1)
+ (read-number (mu4e-format "%s: " prompt) 1)
+ (read-number (mu4e-format "%s (1-%d): " prompt count)))
+ (progn
+ (setq def (if (= count 1) "1" (format "1-%d" count)))
+ (read-string (mu4e-format "%s (default %s): " prompt def)
+ nil nil def)))))
+
+(defun mu4e-view-go-to-url (&optional multi)
+ "Offer to go visit one or more URLs.
+If MULTI (prefix-argument) is non-nil, offer to go to a range of URLs."
+ (interactive "P")
+ (mu4e~view-handle-urls "URL to visit"
+ multi
+ (lambda (url) (mu4e~view-browse-url-from-binding url))))
+
+(defun mu4e-view-save-url (&optional multi)
+ "Offer to save URLs to the kill ring.
+If MULTI (prefix-argument) is nil, save a single one, otherwise, offer
+to save a range of URLs."
+ (interactive "P")
+ (mu4e~view-handle-urls "URL to save" multi
+ (lambda (url)
+ (kill-new url)
+ (mu4e-message "Saved %s to the kill-ring" url))))
+
+(defun mu4e-view-fetch-url (&optional multi)
+ "Offer to fetch (download) URLs.
+If MULTI (prefix-argument) is nil,
+download a single one, otherwise, offer to fetch a range of
+URLs. The urls are fetched to `mu4e-attachment-dir'."
+ (interactive "P")
+ (mu4e~view-handle-urls
+ "URL to fetch" multi
+ (lambda (url)
+ (let ((target (concat (mu4e~get-attachment-dir url) "/"
+ (file-name-nondirectory url))))
+ (url-copy-file url target)
+ (mu4e-message "Fetched %s -> %s" url target)))))
+
+(defun mu4e~view-handle-urls (prompt multi urlfunc)
+ "Handle URLs.
+If MULTI is nil, apply URLFUNC to a single uri, otherwise, apply
+it to a range of uris. PROMPT is the query to present to the user."
+ (if multi
+ (mu4e~view-handle-multi-urls prompt urlfunc)
+ (mu4e~view-handle-single-url prompt urlfunc)))
+
+(defun mu4e~view-handle-single-url (prompt urlfunc &optional num)
+ "Apply URLFUNC to some URL with NUM in the current message.
+Prompting the user with PROMPT for the number."
+ (let* ((num (or num (mu4e~view-get-urls-num prompt)))
+ (url (gethash num mu4e~view-link-map)))
+ (unless url (mu4e-warn "Invalid number for URL"))
+ (funcall urlfunc url)))
+
+(defun mu4e~view-handle-multi-urls (prompt urlfunc)
+ "Apply URLFUNC to a a range of URLs in the current message.
+
+Prompting the user with PROMPT for the numbers.
+
+Default is to apply it to all URLs, [1..n], where n is the number
+of urls. You can type multiple values separated by space, e.g. 1
+3-6 8 will visit urls 1,3,4,5,6 and 8.
+
+Furthermore, there is a shortcut \"a\" which means all urls, but as
+this is the default, you may not need it."
+ (let* ((linkstr (mu4e~view-get-urls-num
+ "URL number range (or 'a' for 'all')" t))
+ (count (hash-table-count mu4e~view-link-map))
+ (linknums (mu4e-split-ranges-to-numbers linkstr count)))
+ (dolist (num linknums)
+ (mu4e~view-handle-single-url prompt urlfunc num))))
+
+(defun mu4e-view-for-each-uri (func)
+ "Evaluate FUNC(uri) for each uri in the current message."
+ (maphash (lambda (_num uri) (funcall func uri)) mu4e~view-link-map))
+
+(defun mu4e-view-message-with-message-id (msgid)
+ "View message with message-id MSGID.
+This (re)creates a
+headers-buffer with a search for MSGID, then open a view for that
+message."
+ (mu4e-search (concat "msgid:" msgid) nil nil t msgid t))
+
+
+;;; Variables
+
+(defvar gnus-icalendar-additional-identities)
+(defvar helm-comp-read-use-marked)
+(defvar-local mu4e~view-rendering nil)
+
+(define-obsolete-variable-alias 'mu4e-view-blocked-images 'gnus-blocked-images
+ "1.5.12")
+(define-obsolete-variable-alias 'mu4e-view-inhibit-images 'gnus-inhibit-images
+ "1.5.12")
+;;; Main
+
+;; remember the mime-handles, so we can clean them up when
+;; we quit this buffer.
+(defvar-local mu4e~gnus-article-mime-handles nil)
+(put 'mu4e~gnus-article-mime-handles 'permanent-local t)
+
+(defun mu4e-view (msg)
+ "Display the message MSG in a new buffer, and keep in sync with HDRSBUF.
+\"In sync\" here means that moving to the next/previous message
+in the the message view affects HDRSBUF, as does marking etc.
+
+As a side-effect, a message that is being viewed loses its
+`unread' marking if it still had that."
+
+ (mu4e~headers-update-handler msg nil nil) ;; update headers, if necessary.
+
+ (when (bufferp gnus-article-buffer)
+ (kill-buffer gnus-article-buffer))
+ (setq gnus-article-buffer mu4e-view-buffer-name)
+ (with-current-buffer (get-buffer-create gnus-article-buffer)
+ (let ((inhibit-read-only t))
+ (remove-overlays (point-min)(point-max) 'mu4e-overlay t)
+ (erase-buffer)
+ (insert-file-contents-literally
+ (mu4e-message-readable-path msg) nil nil nil t)))
+ (switch-to-buffer gnus-article-buffer)
+ (setq mu4e~view-message msg)
+ (mu4e~view-render-buffer msg))
+
+(defun mu4e-view-message-text (msg)
+ "Return the pristine MSG as a string."
+ ;; we need this for replying/forwarding, since the mu4e-compose
+ ;; wants it that way.
+
+ (with-temp-buffer
+ (insert-file-contents-literally
+ (mu4e-message-readable-path msg) nil nil nil t)
+ (mu4e~view-render-buffer msg)
+ (buffer-substring-no-properties (point-min) (point-max))))
+
+(defun mu4e-action-view-in-browser (msg &optional skip-headers)
+ "Show current MSG in browser if it includes an HTML-part.
+If SKIP-HEADERS is set, do not show include message headers.
+The variables `browse-url-browser-function',
+`browse-url-handlers', and `browse-url-default-handlers'
+determine which browser function to use."
+ (with-temp-buffer
+ (insert-file-contents-literally
+ (mu4e-message-readable-path msg) nil nil nil t)
+ (run-hooks 'gnus-article-decode-hook)
+ (let ((header (unless skip-headers
+ (cl-loop for field in '("from" "to" "cc" "date" "subject")
+ when (message-fetch-field field)
+ concat (format "%s: %s\n" (capitalize field) it))))
+ (parts (mm-dissect-buffer t t)))
+ ;; If singlepart, enforce a list.
+ (when (and (bufferp (car parts))
+ (stringp (car (mm-handle-type parts))))
+ (setq parts (list parts)))
+ ;; Process the list
+ (unless (gnus-article-browse-html-parts parts header)
+ (mu4e-warn "Message does not contain a \"text/html\" part"))
+ (mm-destroy-parts parts))))
+
+(defun mu4e-action-view-in-xwidget (msg)
+ "Show current MSG in an embedded xwidget, if available."
+ (unless (fboundp 'xwidget-webkit-browse-url)
+ (mu4e-error "No xwidget support available"))
+ (let ((browse-url-handlers nil)
+ (browse-url-browser-function
+ (lambda (url &optional _rest)
+ (xwidget-webkit-browse-url url))))
+ (mu4e-action-view-in-browser msg)))
+
+(defun mu4e~view-render-buffer (msg)
+ "Render current buffer with MSG using Gnus' article mode."
+ (setq gnus-summary-buffer (get-buffer-create " *appease-gnus*"))
+ (let* ((inhibit-read-only t)
+ (max-specpdl-size mu4e-view-max-specpdl-size)
+ (mm-decrypt-option 'known)
+ (ct (mail-fetch-field "Content-Type"))
+ (ct (and ct (mail-header-parse-content-type ct)))
+ (charset (mail-content-type-get ct 'charset))
+ (charset (and charset (intern charset)))
+ (mu4e~view-rendering t); Needed if e.g. an ics file is buttonized
+ (gnus-article-emulate-mime t)
+ (gnus-unbuttonized-mime-types '(".*/.*"))
+ (gnus-buttonized-mime-types
+ (append (list "multipart/signed" "multipart/encrypted")
+ gnus-buttonized-mime-types))
+ (gnus-newsgroup-charset
+ (if (and charset (coding-system-p charset)) charset
+ (detect-coding-region (point-min) (point-max) t)))
+ ;; Possibly add headers (before "Attachments")
+ (gnus-display-mime-function (mu4e~view-gnus-display-mime msg))
+ (gnus-icalendar-additional-identities
+ (mu4e-personal-addresses 'no-regexp)))
+ (condition-case err
+ (progn
+ (mm-enable-multibyte)
+ (mu4e-view-mode)
+ (run-hooks 'gnus-article-decode-hook)
+ (gnus-article-prepare-display)
+ (mu4e~view-activate-urls)
+ (setq mu4e~gnus-article-mime-handles gnus-article-mime-handles
+ gnus-article-decoded-p gnus-article-decode-hook)
+ (set-buffer-modified-p nil)
+ (add-hook 'kill-buffer-hook #'mu4e~view-kill-mime-handles))
+ (epg-error
+ (mu4e-warn "EPG error: %s; fall back to raw view"
+ (error-message-string err))))))
+
+(defun mu4e~view-kill-mime-handles ()
+ "Kill cached MIME-handles, if any."
+ (when mu4e~gnus-article-mime-handles
+ (mm-destroy-parts mu4e~gnus-article-mime-handles)
+ (setq mu4e~gnus-article-mime-handles nil)))
+
+(defun mu4e-view-refresh ()
+ "Refresh the message view."
+ (interactive)
+ (when (derived-mode-p 'mu4e-view-mode)
+ (kill-buffer)
+ (mu4e-view mu4e~view-message)))
+
+(defun mu4e-view-toggle-show-mime-parts()
+ "Toggle whether to show all MIME-parts."
+ (interactive)
+ (setq gnus-inhibit-mime-unbuttonizing
+ (not gnus-inhibit-mime-unbuttonizing))
+ (mu4e-view-refresh))
+
+(defun mu4e-view-toggle-fill-flowed()
+ "Toggle flowed-message text filling."
+ (interactive)
+ (setq mm-fill-flowed (not mm-fill-flowed))
+ (mu4e-view-refresh))
+
+(defun mu4e~view-gnus-display-mime (msg)
+ "Like `gnus-display-mime' but include mu4e headers to MSG."
+ (lambda (&optional ihandles)
+ (gnus-display-mime ihandles)
+ (unless ihandles
+ (save-restriction
+ (article-goto-body)
+ (forward-line -1)
+ (narrow-to-region (point) (point))
+ (dolist (field mu4e-view-fields)
+ (let ((fieldval (mu4e-message-field msg field)))
+ (pcase field
+ ((or ':path ':maildir :list ':user-agent ':message-id)
+ (mu4e~view-gnus-insert-header field fieldval))
+ (':mailing-list
+ (let ((list (plist-get msg :list)))
+ (if list (mu4e-get-mailing-list-shortname list) "")))
+ ((or ':flags ':tags)
+ (let ((flags (mapconcat (lambda (flag)
+ (if (symbolp flag)
+ (symbol-name flag)
+ flag)) fieldval ", ")))
+ (mu4e~view-gnus-insert-header field flags)))
+ (':size (mu4e~view-gnus-insert-header
+ field (mu4e-display-size fieldval)))
+ ((or ':subject ':to ':from ':cc ':bcc ':from-or-to
+ ':date :attachments ':signature
+ ':decryption)) ; handled by Gnus
+ (_
+ (mu4e~view-gnus-insert-header-custom msg field)))))
+ (let ((gnus-treatment-function-alist
+ '((gnus-treat-highlight-headers
+ gnus-article-highlight-headers))))
+ (gnus-treat-article 'head))))))
+
+(defun mu4e~view-gnus-insert-header (field val)
+ "Insert a header FIELD with value VAL."
+ (let* ((info (cdr (assoc field mu4e-header-info)))
+ (key (plist-get info :name))
+ (help (plist-get info :help)))
+ (if (and val (> (length val) 0))
+ (insert (propertize (concat key ":") 'help-echo help)
+ " " val "\n"))))
+
+(defun mu4e~view-gnus-insert-header-custom (msg field)
+ "Insert MSG's custom FIELD."
+ (let* ((info (cdr-safe (or (assoc field mu4e-header-info-custom)
+ (mu4e-error "Custom field %S not found" field))))
+ (key (plist-get info :name))
+ (func (or (plist-get info :function)
+ (mu4e-error "No :function defined for custom field %S %S"
+ field info)))
+ (val (funcall func msg))
+ (help (plist-get info :help)))
+ (when (and val (> (length val) 0))
+ (insert (propertize (concat key ":") 'help-echo help) " " val "\n"))))
+
+(define-advice gnus-icalendar-event-from-handle
+ (:filter-args (handle-attendee) mu4e~view-fix-missing-charset)
+ "Avoid error when displaying an ical attachment without a charset."
+ (if (and (boundp 'mu4e~view-rendering) mu4e~view-rendering)
+ (let* ((handle (car handle-attendee))
+ (attendee (cadr handle-attendee))
+ (buf (mm-handle-buffer handle))
+ (ty (mm-handle-type handle))
+ (rest (cddr handle)))
+ ;; Put the fallback at the end:
+ (setq ty (append ty '((charset . "utf-8"))))
+ (setq handle (cons buf (cons ty rest)))
+ (list handle attendee))
+ handle-attendee))
+
+(defun mu4e~view-mode-p ()
+ "Is the buffer in mu4e-view-mode or one of its descendants?"
+ (or (eq major-mode 'mu4e-view-mode)
+ (derived-mode-p '(mu4e-view-mode))))
+
+(defun mu4e~view-nop (func &rest args)
+ "Do not invoke FUNC with ARGS when in mu4e-view-mode.
+This is useful for advising some Gnus-functionality that does not work in mu4e."
+ (unless (mu4e~view-mode-p)
+ (apply func args)))
+
+(defun mu4e~view-button-reply (func &rest args)
+ "Advise FUNC with ARGS to make `gnus-button-reply' links work in mu4e."
+ (if (mu4e~view-mode-p)
+ (mu4e-compose-reply)
+ (apply func args)))
+
+(defun mu4e~view-msg-mail (func &rest args)
+ "Advise FUNC with ARGS to make `gnus-msg-mail' links compose with mu4e."
+ (if (mu4e~view-mode-p)
+ (apply 'mu4e~compose-mail args)
+ (apply func args)))
+
+(defvar mu4e-view-mode-map
+ (let ((map (make-keymap)))
+ (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index)
+ (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+
+ (define-key map "q" #'mu4e~view-quit-buffer)
+
+ ;; note, 'z' is by-default bound to 'bury-buffer'
+ ;; but that's not very useful in this case
+ (define-key map "z" #'ignore)
+
+ (define-key map "%" #'mu4e-view-mark-pattern)
+ (define-key map "t" #'mu4e-view-mark-subthread)
+ (define-key map "T" #'mu4e-view-mark-thread)
+ (define-key map "j" #'mu4e~headers-jump-to-maildir)
+
+ (define-key map "g" #'mu4e-view-go-to-url)
+ (define-key map "k" #'mu4e-view-save-url)
+ (define-key map "f" #'mu4e-view-fetch-url)
+
+ (define-key map "F" #'mu4e-compose-forward)
+ (define-key map "R" #'mu4e-compose-reply)
+ (define-key map "C" #'mu4e-compose-new)
+ (define-key map "E" #'mu4e-compose-edit)
+
+ (define-key map "." #'mu4e-view-raw-message)
+ (define-key map "," #'mu4e-sexp-at-point)
+ (define-key map "|" #'mu4e-view-pipe)
+ (define-key map "a" #'mu4e-view-action)
+ (define-key map "A" #'mu4e-view-mime-part-action)
+ (define-key map "e" #'mu4e-view-save-attachments)
+
+ ;; toggle header settings
+ (define-key map "O" #'mu4e-headers-change-sorting)
+ (define-key map "P" #'mu4e-headers-toggle-threading)
+ (define-key map "Q" #'mu4e-headers-toggle-full-search)
+ (define-key map "W" #'mu4e-headers-toggle-include-related)
+
+ ;; change the number of headers
+ (define-key map (kbd "C-+") #'mu4e-headers-split-view-grow)
+ (define-key map (kbd "C--") #'mu4e-headers-split-view-shrink)
+ (define-key map (kbd "<C-kp-add>") #'mu4e-headers-split-view-grow)
+ (define-key map (kbd "<C-kp-subtract>") #'mu4e-headers-split-view-shrink)
+
+ ;; intra-message navigation
+ (define-key map (kbd "S-SPC") #'scroll-down)
+ (define-key map (kbd "SPC") #'mu4e-view-scroll-up-or-next)
+ (define-key map (kbd "RET") #'mu4e-scroll-up)
+ (define-key map (kbd "<backspace>") #'mu4e-scroll-down)
+
+ ;; navigation between messages
+ (define-key map "p" #'mu4e-view-headers-prev)
+ (define-key map "n" #'mu4e-view-headers-next)
+ ;; the same
+ (define-key map (kbd "<M-down>") #'mu4e-view-headers-next)
+ (define-key map (kbd "<M-up>") #'mu4e-view-headers-prev)
+
+ (define-key map (kbd "[") #'mu4e-view-headers-prev-unread)
+ (define-key map (kbd "]") #'mu4e-view-headers-next-unread)
+
+ ;; switching from view <-> headers (when visible)
+ (define-key map "y" #'mu4e-select-other-view)
+
+ ;; marking/unmarking
+ (define-key map "d" #'mu4e-view-mark-for-trash)
+ (define-key map (kbd "<delete>") #'mu4e-view-mark-for-delete)
+ (define-key map (kbd "<deletechar>") #'mu4e-view-mark-for-delete)
+ (define-key map (kbd "D") #'mu4e-view-mark-for-delete)
+ (define-key map (kbd "m") #'mu4e-view-mark-for-move)
+ (define-key map (kbd "r") #'mu4e-view-mark-for-refile)
+
+ (define-key map (kbd "?") #'mu4e-view-mark-for-unread)
+ (define-key map (kbd "!") #'mu4e-view-mark-for-read)
+
+ (define-key map (kbd "+") #'mu4e-view-mark-for-flag)
+ (define-key map (kbd "-") #'mu4e-view-mark-for-unflag)
+ (define-key map (kbd "=") #'mu4e-view-mark-for-untrash)
+ (define-key map (kbd "&") #'mu4e-view-mark-custom)
+
+ (define-key map (kbd "*") #'mu4e-view-mark-for-something)
+ (define-key map (kbd "<kp-multiply>") #'mu4e-view-mark-for-something)
+ (define-key map (kbd "<insert>") #'mu4e-view-mark-for-something)
+ (define-key map (kbd "<insertchar>") #'mu4e-view-mark-for-something)
+
+ (define-key map (kbd "#") #'mu4e-mark-resolve-deferred-marks)
+ ;; misc
+ (define-key map "M" #'mu4e-view-massage)
+
+ (define-key map "w" #'visual-line-mode)
+ (define-key map "h" #'mu4e-view-toggle-html)
+ (define-key map (kbd "M-q") #'article-fill-long-lines)
+
+ (define-key map "c" #'mu4e-copy-thing-at-point)
+
+ ;; next 3 only warn user when attempt in the message view
+ (define-key map "u" #'mu4e-view-unmark)
+ (define-key map "U" #'mu4e-view-unmark-all)
+ (define-key map "x" #'mu4e-view-marked-execute)
+
+ (define-key map "$" #'mu4e-show-log)
+ (define-key map "H" #'mu4e-display-manual)
+
+ ;; menu
+ ;;(define-key map [menu-bar] (make-sparse-keymap))
+ (let ((menumap (make-sparse-keymap)))
+ (define-key map [menu-bar headers] (cons "Mu4e" menumap))
+
+ (define-key menumap [quit-buffer]
+ '("Quit view" . mu4e~view-quit-buffer))
+ (define-key menumap [display-help] '("Help" . mu4e-display-manual))
+
+ (define-key menumap [sepa0] '("--"))
+ (define-key menumap [wrap-lines]
+ '("Toggle wrap lines" . visual-line-mode))
+ (define-key menumap [raw-view]
+ '("View raw message" . mu4e-view-raw-message))
+ (define-key menumap [pipe]
+ '("Pipe through shell" . mu4e-view-pipe))
+
+ (define-key menumap [sepa1] '("--"))
+ (define-key menumap [mark-delete]
+ '("Mark for deletion" . mu4e-view-mark-for-delete))
+ (define-key menumap [mark-untrash]
+ '("Mark for untrash" . mu4e-view-mark-for-untrash))
+ (define-key menumap [mark-trash]
+ '("Mark for trash" . mu4e-view-mark-for-trash))
+ (define-key menumap [mark-move]
+ '("Mark for move" . mu4e-view-mark-for-move))
+
+ (define-key menumap [sepa2] '("--"))
+ (define-key menumap [resend] '("Resend" . mu4e-compose-resend))
+ (define-key menumap [forward] '("Forward" . mu4e-compose-forward))
+ (define-key menumap [reply] '("Reply" . mu4e-compose-reply))
+ (define-key menumap [compose-new] '("Compose new" . mu4e-compose-new))
+ (define-key menumap [sepa3] '("--"))
+
+ (define-key menumap [query-next]
+ '("Next query" . mu4e-headers-query-next))
+ (define-key menumap [query-prev]
+ '("Previous query" . mu4e-headers-query-prev))
+ (define-key menumap [narrow-search]
+ '("Narrow search" . mu4e-headers-search-narrow))
+ (define-key menumap [bookmark]
+ '("Search bookmark" . mu4e-headers-search-bookmark))
+ (define-key menumap [jump]
+ '("Jump to maildir" . mu4e~headers-jump-to-maildir))
+ (define-key menumap [search]
+ '("Search" . mu4e-headers-search))
+
+ (define-key menumap [sepa4] '("--"))
+ (define-key menumap [next] '("Next" . mu4e-view-headers-next))
+ (define-key menumap [previous] '("Previous" . mu4e-view-headers-prev)))
+
+ ;; Make 0..9 shortcuts for digit-argument. Actually, none of the bound
+ ;; functions seem to use a prefix arg but those bindings existed because we
+ ;; used to use `suppress-keymap'. And possibly users added their own
+ ;; prefix arg consuming commands.
+ (dotimes (i 10)
+ (define-key map (kbd (format "%d" i)) #'digit-argument))
+
+ (set-keymap-parent map special-mode-map)
+ map)
+ "Keymap for mu4e-view mode.")
+
+(set-keymap-parent mu4e-view-mode-map button-buffer-map)
+
+(defcustom mu4e-view-mode-hook nil
+ "Hook run when entering Mu4e-View mode."
+ :options '(turn-on-visual-line-mode)
+ :type 'hook
+ :group 'mu4e-view)
+
+;; "Define the major-mode for the mu4e-view."
+(define-derived-mode mu4e-view-mode gnus-article-mode "mu4e:view"
+ "Major mode for viewing an e-mail message in mu4e.
+Based on Gnus' article-mode."
+ ;; Restore C-h b default behavior
+ (define-key mu4e-view-mode-map (kbd "C-h b") 'describe-bindings)
+ ;; ;; turn off gnus modeline changes and menu items
+ (advice-add 'gnus-set-mode-line :around #'mu4e~view-nop)
+ (advice-add 'gnus-button-reply :around #'mu4e~view-button-reply)
+ (advice-add 'gnus-msg-mail :around #'mu4e~view-msg-mail)
+
+ ;; advice gnus-block-private-groups to always return "."
+ ;; so that by default we block images.
+ (advice-add 'gnus-block-private-groups :around
+ (lambda(func &rest args)
+ (if (mu4e~view-mode-p)
+ "." (apply func args))))
+ (use-local-map mu4e-view-mode-map)
+ (mu4e-context-minor-mode)
+ (mu4e-search-minor-mode)
+ (setq buffer-undo-list t);; don't record undo info
+
+ ;; support bookmarks.
+ (set (make-local-variable 'bookmark-make-record-function)
+ 'mu4e--make-bookmark-record)
+
+ ;; autopair mode gives error when pressing RET
+ ;; turn it off
+ (when (boundp 'autopair-dont-activate)
+ (setq autopair-dont-activate t)))
+
+;;; Massaging the message view
+
+(defcustom mu4e-view-massage-options
+ '( ("ctoggle citations" . gnus-article-hide-citation)
+ ("htoggle headers" . gnus-article-hide-headers)
+ ("ytoggle crypto" . gnus-article-hide-pem)
+ ("ftoggle fill-flowed" . mu4e-view-toggle-fill-flowed)
+ ("mtoggle show all MIME parts" . mu4e-view-toggle-show-mime-parts))
+"Various options for \"massaging\" the message view. See `(gnus)
+Article Treatment' for more options."
+ :group 'mu4e-view
+ :type '(alist :key-type string :value-type function))
+
+(defun mu4e-view-massage()
+ "Massage current message view as per `mu4e-view-massage-options'."
+ (interactive)
+ (funcall (mu4e-read-option "Massage: " mu4e-view-massage-options)))
+
+;;; MIME-parts
+(defvar-local mu4e~view-mime-parts nil
+ "MIME parts for this message.")
+
+(defun mu4e~view-gather-mime-parts ()
+ "Gather all MIME parts as an alist.
+The alist uniquely maps the number to the gnus-part."
+ (let ((parts '()))
+ (save-excursion
+ (goto-char (point-min))
+ (while (not (eobp))
+ (let ((part (get-text-property (point) 'gnus-data))
+ (index (get-text-property (point) 'gnus-part)))
+ (when (and part (numberp index) (not (assoc index parts))
+ (push `(,index . ,part) parts)))
+ (goto-char (or (next-single-property-change (point) 'gnus-part)
+ (point-max))))))
+ parts))
+
+
+(defun mu4e-view-save-attachments (&optional arg)
+ "Save MIME-parts from current mu4e gnus view buffer.
+
+When helm-mode is enabled provide completion on attachments and
+possibility to mark candidates to save, otherwise completion on
+attachments is done with `completing-read-multiple', in this case
+use \",\" to separate candidate, completion is provided after
+each \",\".
+
+ARG is specific for the handler, see below.
+
+Note, currently this does not work well with file names
+containing commas."
+ (interactive "P")
+ (cl-assert (and (eq major-mode 'mu4e-view-mode)
+ (derived-mode-p 'gnus-article-mode)))
+ (let* ((parts (mu4e~view-gather-mime-parts))
+ (handles '())
+ (files '())
+ (compfn (if (and (boundp 'helm-mode) helm-mode)
+ #'completing-read
+ ;; Fallback to `completing-read-multiple' with poor
+ ;; completion
+ #'completing-read-multiple))
+ dir)
+ (dolist (part parts)
+ (let ((fname (or (cdr (assoc 'filename (assoc "attachment" (cdr part))))
+ (cl-loop for item in part
+ for name = (and (listp item)
+ (assoc-default 'name item))
+ thereis (and (stringp name) name)))))
+ (when fname
+ (push `(,fname . ,(cdr part)) handles)
+ (push fname files))))
+ (if files
+ (progn
+ (setq files (let ((helm-comp-read-use-marked t))
+ (funcall compfn "Save part(s): " files))
+ dir (if arg (read-directory-name "Save to directory: ")
+ mu4e-attachment-dir))
+ (cl-loop for (f . h) in handles
+ when (member f files)
+ do (mm-save-part-to-file
+ h (let ((file (expand-file-name f dir)))
+ (if (file-exists-p file)
+ (let (newname (count 1))
+ (while (and
+ (setq newname
+ (concat
+ (file-name-sans-extension file)
+ (format "(%s)" count)
+ (file-name-extension file t)))
+ (file-exists-p newname))
+ (cl-incf count))
+ newname)
+ file)))))
+ (mu4e-message "No attached files found"))))
+
+
+(defvar mu4e-view-mime-part-actions
+ '(
+ ;;
+ ;; some basic ones
+ ;;
+
+ ;; save MIME-part to a file
+ (:name "save" :handler gnus-article-save-part :receives index)
+ ;; pipe MIME-part to some arbitrary shell command
+ (:name "|pipe" :handler gnus-article-pipe-part :receives index)
+ ;; open with the default handler, if any
+ (:name "open" :handler mu4e~view-open-file :receives temp)
+ ;; open with some custom file.
+ (:name "wopen-with" :handler (lambda (file)(mu4e~view-open-file file t))
+ :receives temp)
+
+ ;;
+ ;; some more examples
+ ;;
+
+ ;; import GPG key
+ (:name "gpg" :handler epa-import-keys :receives temp)
+ ;; count the number of lines in a MIME-part
+ (:name "line-count" :handler "wc -l" :receives pipe)
+ ;; open in this emacs instance; tries to use the attachment name,
+ ;; so emacs can use specific modes etc.
+ (:name "emacs" :handler find-file-read-only :receives temp)
+ ;; open in this emacs instance, "raw"
+ (:name "raw" :handler (lambda (str)
+ (let ((tmpbuf
+ (get-buffer-create " *mu4e-raw-mime*")))
+ (with-current-buffer tmpbuf
+ (insert str)
+ (view-mode)
+ (goto-char (point-min)))
+ (switch-to-buffer tmpbuf))) :receives pipe))
+
+ "Specifies actions for MIME-parts.
+
+Each of the actions is a plist with keys
+`(:name <name> ;; name of the action; shortcut is first letter of name
+
+ :handler ;; one of:
+ ;; - a function receiving the index/temp/pipe
+ ;; - a string, which is taken as a shell command
+
+ :receives ;; a symbol specifying what the handler receives
+ ;; - index: the index number of the mime part (default)
+ ;; - temp: the full path to the mime part in a
+ ;; temporary file, which is deleted immediately
+ ;; after invoking handler
+ ;; - pipe: the attachment is piped to some shell command
+ ;; or as a string parameter to a function
+).")
+
+
+(defun mu4e~view-mime-part-to-temp-file (handle)
+ "Write MIME-part HANDLE to a temporary file and return the file name.
+The filename is deduced from the MIME-part's filename, or
+otherwise random; the result is placed in a temporary directory
+with a unique name. Returns the full path for the file created.
+The directory and file are self-destructed."
+ (let* ((tmpdir (make-temp-file "mu4e-temp-" t))
+ (fname (mm-handle-filename handle))
+ (fname (and fname
+ (gnus-map-function mm-file-name-rewrite-functions
+ (file-name-nondirectory fname))))
+ (fname (if fname
+ (concat tmpdir "/" (replace-regexp-in-string "/" "-" fname))
+ (let ((temporary-file-directory tmpdir))
+ (make-temp-file "mimepart")))))
+ (mm-save-part-to-file handle fname)
+ (run-at-time "30 sec" nil
+ (lambda () (ignore-errors (delete-directory tmpdir t))))
+ fname))
+
+
+(defun mu4e~view-open-file (file &optional force-ask)
+ "Open FILE with default handler, if any.
+Otherwise, or if FORCE-ASK is set, ask user for the program to
+open with."
+ (if (and (not force-ask)
+ (functionp mu4e-view-open-program))
+ (funcall mu4e-view-open-program file)
+ (let ((opener
+ (or (and (not force-ask) mu4e-view-open-program
+ (executable-find mu4e-view-open-program))
+ (read-shell-command "Open MIME-part with: "))))
+ (call-process opener nil 0 nil file))))
+
+(defun mu4e-view-mime-part-action (&optional n)
+ "Apply some action to MIME-part N in the current messsage.
+If N is not specified, ask for it. For instance, '3 A o' opens
+the third MIME-part."
+ (interactive "NNumber of MIME-part: ")
+ (let* ((parts (mu4e~view-gather-mime-parts))
+ (options
+ (mapcar (lambda (action) `(,(plist-get action :name) . ,action))
+ mu4e-view-mime-part-actions))
+ (handle
+ (or (cdr-safe (seq-find (lambda (part) (eq (car part) n)) parts))
+ (mu4e-error "MIME-part %s not found" n)))
+ (action
+ (or (and options (mu4e-read-option "Action on MIME-part: " options))
+ (mu4e-error "No such action")))
+ (handler
+ (or (plist-get action :handler)
+ (mu4e-error "No :handler item found for action %S" action)))
+ (receives
+ (or (plist-get action :receives)
+ (mu4e-error "No :receives item found for action %S" action))))
+ (save-excursion
+ (cond
+ ((functionp handler)
+ (cond
+ ((eq receives 'index) (funcall handler n))
+ ((eq receives 'pipe) (funcall handler (mm-with-unibyte-buffer
+ (mm-insert-part handle)
+ (buffer-string))))
+ ((eq receives 'temp)
+ (funcall handler (mu4e~view-mime-part-to-temp-file handle)))
+ (t (mu4e-error "Invalid :receive for %S" action))))
+ ((stringp handler)
+ (cond
+ ((eq receives 'index)
+ (shell-command (concat handler " " (shell-quote-argument n))))
+ ((eq receives 'pipe) (mm-pipe-part handle handler))
+ ((eq receives 'temp)
+ (shell-command
+ (shell-command (concat handler " "
+ (shell-quote-argument
+ (mu4e~view-mime-part-to-temp-file handle))))))
+ (t (mu4e-error "Invalid action %S" action))))))))
+
+(defun mu4e-view-toggle-html ()
+ "Toggle html-display of the first html-part found."
+ (interactive)
+ ;; This function assumes `gnus-article-mime-handle-alist' is sorted by
+ ;; pertinence, i.e. the first HTML part found in it is the most important one.
+ (save-excursion
+ (if-let ((html-part
+ (seq-find (lambda (handle)
+ (equal (mm-handle-media-type (cdr handle)) "text/html"))
+ gnus-article-mime-handle-alist)))
+ (gnus-article-inline-part (car html-part))
+ (mu4e-warn "No html part in this message"))))
+
+(defun mu4e-process-file-through-pipe (path pipecmd)
+ "Process file at PATH through a pipe with PIPECMD."
+ (let ((buf (get-buffer-create "*mu4e-output")))
+ (with-current-buffer buf
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (call-process-shell-command pipecmd path t t)
+ (view-mode)))
+ (switch-to-buffer buf)))
+\f
+;;; Bug Reference mode support
+
+;; This is Emacs 28 stuff but there is no need to guard it with some (f)boundp
+;; checks (which would return nil if bug-reference.el is not loaded before
+;; mu4e) since the function definition doesn't hurt and `add-hook' works fine
+;; for not yet defined variables (by creating them).
+(declare-function bug-reference-maybe-setup-from-mail "ext:bug-reference")
+(defun mu4e--view-try-setup-bug-reference-mode ()
+ "Try to guess bug-reference setup from the current mu4e mail.
+Looks at the maildir and the mail headers List, List-Id, Maildir,
+To, From, Cc, and Subject and tries to guess suitable values for
+`bug-reference-bug-regexp' and `bug-reference-url-format' by
+matching the maildir name against GROUP-REGEXP and each header
+value against HEADER-REGEXP in
+`bug-reference-setup-from-mail-alist'."
+ (when (derived-mode-p 'mu4e-view-mode)
+ (let (header-values)
+ (save-excursion
+ (goto-char (point-min))
+ (dolist (field '("list" "list-id" "to" "from" "cc" "subject"))
+ (let ((val (mail-fetch-field field)))
+ (when val
+ (push val header-values)))))
+ (bug-reference-maybe-setup-from-mail
+ (mail-fetch-field "maildir")
+ header-values))))
+
+(add-hook 'bug-reference-auto-setup-functions
+ #'mu4e--view-try-setup-bug-reference-mode)
+
+\f
+(provide 'mu4e-view)
+;;; mu4e-view.el ends here
--- /dev/null
+;;; mu4e.el --- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: email
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'mu4e-vars)
+(require 'mu4e-helpers)
+(require 'mu4e-folders)
+(require 'mu4e-context)
+(require 'mu4e-contacts)
+(require 'mu4e-headers)
+(require 'mu4e-view)
+(require 'mu4e-compose)
+(require 'mu4e-bookmarks)
+(require 'mu4e-update)
+(require 'mu4e-main)
+(require 'mu4e-server) ;; communication with backend
+
+\f
+
+(defcustom mu4e-confirm-quit t
+ "Whether to confirm to quit mu4e."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-org-support t
+ "Support Org-mode links."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-speedbar-support nil
+ "Support having a speedbar to navigate folders/bookmarks."
+ :type 'boolean
+ :group 'mu4e)
+
+(when mu4e-speedbar-support
+ (require 'mu4e-speedbar)) ;; support for speedbar
+(when mu4e-org-support
+ (require 'mu4e-org)) ;; support for org-mode links
+
+;; We can't properly use compose buffers that are revived using
+;; desktop-save-mode; so let's turn that off.
+(with-eval-after-load 'desktop
+ (eval '(add-to-list 'desktop-modes-not-to-save 'mu4e-compose-mode)))
+\f
+;;;###autoload
+(defun mu4e (&optional background)
+ "If mu4e is not running yet, start it.
+Then, show the main window, unless BACKGROUND (prefix-argument)
+is non-nil."
+ (interactive "P")
+ ;; start mu4e, then show the main view
+ (mu4e--init-handlers)
+ (mu4e--start (unless background 'mu4e--main-view)))
+
+(defun mu4e-quit()
+ "Quit the mu4e session."
+ (interactive)
+ (if mu4e-confirm-quit
+ (when (y-or-n-p (mu4e-format "Are you sure you want to quit?"))
+ (mu4e--stop))
+ (mu4e--stop)))
+\f
+;;; Internals
+
+(defun mu4e--check-requirements ()
+ "Check for the settings required for running mu4e."
+ (unless (>= emacs-major-version 25)
+ (mu4e-error "Emacs >= 25.x is required for mu4e"))
+ (when (mu4e-server-properties)
+ (unless (string= (mu4e-server-version) mu4e-mu-version)
+ (mu4e-error "The mu server has version %s, but we need %s"
+ (mu4e-server-version) mu4e-mu-version)))
+ (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
+ (mu4e-error "Please set `mu4e-mu-binary' to the full path to the mu
+ binary"))
+ (dolist (var '(mu4e-sent-folder mu4e-drafts-folder
+ mu4e-trash-folder))
+ (unless (and (boundp var) (symbol-value var))
+ (mu4e-error "Please set %S" var))
+ (unless (functionp (symbol-value var)) ;; functions are okay, too
+ (let* ((dir (symbol-value var))
+ (path (concat (mu4e-root-maildir) dir)))
+ (unless (string= (substring dir 0 1) "/")
+ (mu4e-error "%S must start with a '/'" dir))
+ (unless (mu4e-create-maildir-maybe path)
+ (mu4e-error "%s (%S) does not exist" path var))))))
+
+;;; Starting / getting mail / updating the index
+
+(defun mu4e--pong-handler (_data func)
+ "Handle \"pong\" responses from the mu server.
+Invoke FUNC if non-nil."
+ (let ((doccount (plist-get (mu4e-server-properties) :doccount)))
+ (mu4e--check-requirements)
+ (when func (funcall func))
+ (when (zerop doccount)
+ (mu4e-message "Store is empty; try indexing (M-x mu4e-update-index)."))
+ (when (and mu4e-update-interval (null mu4e--update-timer))
+ (setq mu4e--update-timer
+ (run-at-time 0 mu4e-update-interval
+ (lambda () (mu4e-update-mail-and-index
+ mu4e-index-update-in-background)))))))
+
+(defun mu4e--start (&optional func)
+ "Start mu4e.
+If `mu4e-contexts' have been defined, but we don't have a context
+yet, switch to the matching one, or none matches, the first. If
+mu4e is already running, invoke FUNC (if non-nil).
+
+Otherwise, check requirements, then start mu4e. When successful,
+invoke
+ FUNC (if non-nil) afterwards."
+ (unless (mu4e-context-current)
+ (mu4e--context-autoswitch nil mu4e-context-policy))
+ (setq mu4e-pong-func (lambda (info) (mu4e--pong-handler info func)))
+ (mu4e--server-ping
+ (mapcar ;; send it a list of queries we'd like to see read/unread info for
+ (lambda (bm)
+ (funcall (or mu4e-query-rewrite-function #'identity)
+ (plist-get bm :query)))
+ ;; exclude bookmarks that are not strings, and with certain flags
+ (seq-filter (lambda (bm)
+ (and (stringp (plist-get bm :query))
+ (not (or (plist-get bm :hide)
+ (plist-get bm :hide-unread)))))
+ (append (mu4e-bookmarks)
+ (mu4e--maildirs-with-query)))))
+ ;; maybe request the list of contacts, automatically refreshed after
+ ;; reindexing
+ (unless mu4e--contacts-set (mu4e--request-contacts-maybe)))
+
+(defun mu4e--stop ()
+ "Stop mu4e."
+ (when mu4e--update-timer
+ (cancel-timer mu4e--update-timer)
+ (setq mu4e--update-timer nil))
+ (mu4e-clear-caches)
+ (mu4e--server-kill)
+ ;; kill all mu4e buffers
+ (mapc
+ (lambda (buf)
+ ;; When using the Gnus-based viewer, the view buffer has the
+ ;; kill-buffer-hook function mu4e~view-kill-buffer-hook-fn which kills the
+ ;; mm-* buffers created by Gnus' article mode. Those have been returned by
+ ;; `buffer-list' but might already be deleted in case the view buffer has
+ ;; been killed first. So we need a `buffer-live-p' check here.
+ (when (buffer-live-p buf)
+ (with-current-buffer buf
+ (when (member major-mode
+ '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode))
+ (kill-buffer)))))
+ (buffer-list)))
+\f
+;;; Handlers
+(defun mu4e--default-handler (&rest args)
+ "Dummy handler function with arbitrary ARGS."
+ (mu4e-error "Not handled: %S" args))
+
+(defun mu4e--error-handler (errcode errmsg)
+ "Handler function for showing an error with ERRCODE and ERRMSG."
+ ;; don't use mu4e-error here; it's running in the process filter context
+ (pcase errcode
+ ('4 (mu4e-warn "No matches for this search query."))
+ ('110 (display-warning 'mu4e errmsg :error)) ;; schema version.
+ (_ (mu4e-error "Error %d: %s" errcode errmsg))))
+
+(defun mu4e--update-status (info)
+ "Update the status message with INFO."
+ (setq mu4e-index-update-status
+ `(:tstamp ,(current-time)
+ :checked ,(plist-get info :checked)
+ :updated ,(plist-get info :updated)
+ :cleaned-up ,(plist-get info :cleaned-up))))
+
+(defun mu4e--info-handler (info)
+ "Handler function for (:INFO ...) sexps received from server."
+ (let* ((type (plist-get info :info))
+ (checked (plist-get info :checked))
+ (updated (plist-get info :updated))
+ (cleaned-up (plist-get info :cleaned-up))
+ (mainbuf (get-buffer mu4e-main-buffer-name)))
+ (cond
+ ((eq type 'add) t) ;; do nothing
+ ((eq type 'index)
+ (if (eq (plist-get info :status) 'running)
+ (mu4e-index-message
+ "Indexing... checked %d, updated %d" checked updated)
+ (progn ;; i.e. 'complete
+ (mu4e--update-status info)
+ (mu4e-index-message
+ "%s completed; checked %d, updated %d, cleaned-up %d"
+ (if mu4e-index-lazy-check "Lazy indexing" "Indexing")
+ checked updated cleaned-up)
+ (run-hooks 'mu4e-index-updated-hook)
+ ;; backward compatibility...
+ (unless (zerop (+ updated cleaned-up))
+ mu4e-message-changed-hook)
+ (unless (and (not (string= mu4e--contacts-tstamp "0"))
+ (zerop (plist-get info :updated)))
+ (mu4e--request-contacts-maybe))
+ (when (and (buffer-live-p mainbuf) (get-buffer-window mainbuf))
+ (save-window-excursion
+ (select-window (get-buffer-window mainbuf))
+ (mu4e--main-view 'refresh))))))
+ ((plist-get info :message)
+ (mu4e-index-message "%s" (plist-get info :message))))))
+
+(defun mu4e--init-handlers()
+ "Initialize the server message handlers.
+Only set set them if they were nil before, so overriding has a
+chance."
+ (mu4e-setq-if-nil mu4e-error-func #'mu4e--error-handler)
+ (mu4e-setq-if-nil mu4e-update-func #'mu4e~headers-update-handler)
+ (mu4e-setq-if-nil mu4e-remove-func #'mu4e~headers-remove-handler)
+ (mu4e-setq-if-nil mu4e-view-func #'mu4e~headers-view-handler)
+ (mu4e-setq-if-nil mu4e-headers-append-func #'mu4e~headers-append-handler)
+ (mu4e-setq-if-nil mu4e-found-func #'mu4e~headers-found-handler)
+ (mu4e-setq-if-nil mu4e-erase-func #'mu4e~headers-clear)
+
+ (mu4e-setq-if-nil mu4e-sent-func #'mu4e--default-handler)
+ (mu4e-setq-if-nil mu4e-compose-func #'mu4e~compose-handler)
+ (mu4e-setq-if-nil mu4e-contacts-func #'mu4e--update-contacts)
+ (mu4e-setq-if-nil mu4e-info-func #'mu4e--info-handler)
+ (mu4e-setq-if-nil mu4e-pong-func #'mu4e--default-handler))
+\f
+(defun mu4e-clear-caches ()
+ "Clear any cached resources."
+ (setq
+ mu4e-maildir-list nil
+ mu4e--contacts-set nil
+ mu4e--contacts-tstamp "0"))
+;;; _
+(provide 'mu4e)
+;;; mu4e.el ends here
--- /dev/null
+\input texinfo.tex @c -*-texinfo-*-
+@documentencoding UTF-8
+@include version.texi
+@c %**start of header
+@setfilename mu4e.info
+@settitle Mu4e @value{VERSION} user manual
+
+@c Use proper quote and backtick for code sections in PDF output
+@c Cf. Texinfo manual 14.2
+@set txicodequoteundirected
+@set txicodequotebacktick
+@c %**end of header
+
+@copying
+Copyright @copyright{} 2012-2022 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{Mu4e} --- an e-mail client for GNU Emacs
+@subtitle version @value{VERSION}, @value{UPDATED}
+@author Dirk-Jan C. Binnema
+
+@c The following two commands start the copyright page.
+@page
+@vskip 0pt plus 1filll
+@insertcopying
+@end titlepage
+
+@dircategory Emacs
+@direntry
+* Mu4e: (Mu4e). An email client for GNU Emacs.
+@end direntry
+
+@contents
+
+@ifnottex
+@node Top
+@top mu4e manual
+@end ifnottex
+
+@iftex
+@node Welcome to mu4e
+@unnumbered Welcome to mu4e
+@end iftex
+
+Welcome to @t{mu4e} @value{VERSION}.
+
+@t{mu4e} (@t{mu}-for-emacs) is an e-mail client for GNU Emacs version
+25.3 or newer, built on top of the
+@t{mu}@footnote{@url{https://www.djcbsoftware.nl/code/mu}} e-mail search
+engine. @t{mu4e} is optimized for quickly processing large amounts of
+e-mail.
+
+Some of its highlights:
+@itemize
+@item Fully search-based: there are no folders@footnote{that is, instead of
+folders, you use queries that match messages in a particular folder},
+only queries.
+@item Fully documented, with example configurations
+@item User-interface optimized for speed, with quick key strokes for common actions
+@item Support for non-English languages (so ``angstrom'' matches ``Ångström'')
+@item Asynchronous: heavy actions don't block @t{emacs}@footnote{currently,
+the only exception to this is @emph{sending mail}; there are solutions
+for that though --- see the @ref{FAQ}}
+@item Support for cryptography --- signing, encrypting and decrypting
+@item Address auto-completion based on the contacts in your messages
+@item Extendable with your own snippets of elisp
+@end itemize
+
+In this manual, we go through the installation of @t{mu4e}, do some
+basic configuration and explain its daily use. We also show you how you
+can customize @t{mu4e} for your special needs.
+
+At the end of the manual, there are some example configurations, to get
+you up to speed quickly: @ref{Example configs}. There's also a section
+with answers to frequently asked questions, @ref{FAQ}.
+
+@menu
+* Introduction:: Where to begin
+* Getting started:: Setting things up
+* Main view:: The @t{mu4e} overview
+* Headers view:: Lists of message headers
+* Message view:: Viewing specific messages
+* Editor view:: Creating and editing messages
+* Searching:: Some more background on searching/queries`
+* Marking:: Marking messages and performing actions
+* Contexts:: Defining contexts and switching between them
+* Dynamic folders:: Folders that change based on circumstances
+* Actions:: Defining and using custom actions
+* Extending mu4e:: Writing code for @t{mu4e}
+
+Appendices
+* Other tools:: mu4e and the rest of the world
+* Example configs:: Some examples to set you up quickly
+* FAQ:: Common questions and answers
+* Tips and Tricks:: Useful tips
+* How it works:: Some notes about the implementation of @t{mu4e}
+* Debugging:: How to debug problems in @t{mu4e}
+
+* GNU Free Documentation License:: The license of this manual
+@end menu
+
+@node Introduction
+@chapter Introduction
+
+Let's get started!
+
+@menu
+* Why another e-mail client::Aren't there enough already
+* Other mail clients::Where @t{mu4e} takes its inspiration from
+* What mu4e does not do::Focus on the core-business, delegate the rest
+* Becoming a mu4e user::Joining the club
+@end menu
+
+@node Why another e-mail client
+@section Why another e-mail client?
+
+I (@t{mu4e}'s author) spend a @emph{lot} of time dealing with e-mail,
+both professionally and privately. Having an efficient e-mail client
+is essential. Since none of the existing ones worked the way I wanted,
+I thought about creating my own.
+
+Emacs is an integral part of my workflow, so it made a lot of
+sense to use it for e-mail as well. And as I had already written an
+e-mail search engine (@t{mu}), it seemed only logical to use that as a
+basis.
+
+@node Other mail clients
+@section Other mail clients
+
+Under the hood, @t{mu4e} is fully search-based, similar to programs
+like @t{notmuch}@footnote{@url{https://notmuchmail.org/}} and
+@t{sup}@footnote{@url{https://sup-heliotrope.github.io/}}.
+
+However, @t{mu4e}'s user-interface is quite different. @t{mu4e}'s mail
+handling (deleting, moving, etc.)@: is inspired by
+Wanderlust@footnote{@url{http://www.gohome.org/wl/}} (another
+Emacs-based e-mail client),
+@t{mutt}@footnote{@url{http://www.mutt.org/}} and the @t{dired}
+file-manager for emacs.
+
+@t{mu4e} keeps all the `state' in your maildirs, so you can easily
+switch between clients, synchronize over @abbr{IMAP}, backup with
+@t{rsync} and so on. The Xapian-database that @t{mu} maintains is
+merely a @emph{cache}; if you delete it, you won't lose any
+information.
+
+@node What mu4e does not do
+@section What @t{mu4e} does not do
+
+There are a number of things that @t{mu4e} does @b{not} do, by design:
+@itemize
+@item @t{mu}/@t{mu4e} do @emph{not} get your e-mail messages from
+a mail server. Nor does it sync-back any changes. Those tasks are
+delegated to other tools, such as
+@t{offlineimap}@footnote{@url{https://www.offlineimap.org/}},
+@t{isync/mbsync}@footnote{@url{http://isync.sourceforge.net/}} or
+@t{fetchmail}@footnote{@url{http://www.fetchmail.info/}}; As long as
+the messages end up in a maildir, @t{mu4e} and @t{mu} are happy to
+deal with them.
+@item @t{mu4e} also does @emph{not} implement sending of messages; instead, it depends on @t{smtpmail} (@ref{Top,,smtpmail}), which is part of Emacs. In addition, @t{mu4e} piggybacks on Gnus' message editor.
+@end itemize
+
+Thus, many of the things an e-mail client traditionally needs to do, are
+delegated to other tools. This leaves @t{mu4e} to concentrate on what it does
+best: quickly finding the mails you are looking for, and handle them as
+efficiently as possible.
+
+@node Becoming a mu4e user
+@section Becoming a @t{mu4e} user
+
+If @t{mu4e} sounds like something for you, give it a shot! We're trying
+hard to make it as easy as possible to set up and use; and while you can
+use elisp in various places to augment @t{mu4e}, a lot of knowledge
+about programming or elisp shouldn't be required. The idea is to provide
+sensible defaults, and allow for customization.
+
+When you take @t{mu4e} into use, it's a good idea to subscribe to the
+@t{mu}/@t{mu4e}-mailing
+list@footnote{@url{https://groups.google.com/group/mu-discuss}}.
+
+Sometimes, you might encounter some unexpected behavior while using
+@t{mu4e}. It could be a bug in @t{mu4e}, it could be an issue in other
+software. Or it could just be a misunderstanding. In any case, if you
+want to report this (either to the mailing list or to
+@url{https://github.com/djcb/mu/issues}, the latter is preferred),
+please always include the following information:
+
+@itemize
+@item what did you expect that should happen? what actually happened?
+@item can you provide some exact steps to reproduce?
+@item what version of @t{mu4e} and @t{emacs} were you using? What operating system?
+@item can you reproduce it with @command{emacs -q} and only loading @t{mu4e}?
+@item if the problem is related to some specific message, please include the raw message file (appropriately anonymized, of course)
+@end itemize
+
+@node Getting started
+@chapter Getting started
+
+In this chapter, we go through the installation of @t{mu4e} and its
+basic setup. After we have succeeded in @ref{Getting mail}, and
+@pxref{Indexing your messages}, we discuss the @ref{Basic
+configuration}.
+
+After these steps, @t{mu4e} should be ready to go!
+
+@menu
+* Requirements:: What is needed
+* Versions:: Available stable and development versions
+* Installation:: How to install @t{mu} and @t{mu4e}
+* Getting mail:: Getting mail from a server
+* Initializing the message store:: Settings things up
+* Indexing your messages:: Creating and maintaining the index
+* Basic configuration:: Settings for @t{mu4e}
+* Folders:: Setting up standard folders
+* Retrieval and indexing:: Doing it from @t{mu4e}
+* Sending mail:: How to send mail
+* Running mu4e:: Overview of the @t{mu4e} views
+
+@end menu
+
+@node Requirements
+@section Requirements
+
+@t{mu}/@t{mu4e} are known to work on a wide variety of Unix- and
+Unix-like systems, including many Linux distributions, OS X and FreeBSD,
+and even on MS-Windows (with Cygwin). Emacs 25.3 or higher is required,
+as well as Xapian@footnote{@url{https://xapian.org/}} and
+GMime@footnote{@url{http://spruce.sourceforge.net/gmime/}}.
+
+@t{mu} has optional support for both versions 2.2 and 3.0 of the Guile
+(Scheme) programming language. There are also some GUI-toys, which
+require GTK+ 3.x and Webkit.
+
+If you intend to compile @t{mu} yourself, you need to have the typical
+development tools, such as C and C++17 compilers (both @command{gcc} and
+@command{clang} should work), @command{meson} and @command{make}, and
+the development packages for GMime 3.x, GLib and Xapian. Optionally, you
+also need the development packages for GTK+, Webkit and Guile.
+
+@node Versions
+@section Versions
+
+The stable (release) versions have even minor version numbers, while the
+development versions have odd ones. So, for example, 1.6.10 is a stable
+version, while the 1.7.15 is the development version.
+
+The stable versions only receive bug fixes after being released, while
+the development versions get new features, fixes, and, perhaps, bugs,
+and are meant for people with a tolerance for that.
+
+There is support for one release branch; so, when the 1.8 release is
+available (and a new 1.9 development series start), no more changes are
+expected for the 1.6 releases.
+
+@node Installation
+@section Installation
+
+@t{mu4e} is part of @t{mu} --- by installing the latter, the former is
+installed as well. Some Linux distributions provide packaged versions of
+@t{mu}/@t{mu4e}; if you can use those, there is no need to compile
+anything yourself. However, if there are no packages for your
+distribution, if they are outdated, or if you want to use the latest
+development versions, you can follow the steps below.
+
+First, you need make sure you have the necessary dependencies; the
+details depend on your distribution. If you're using another
+distribution (or another OS), the below can at least be helpful in
+identifying the packages to install.
+
+We provide some instructions for Debian, Ubuntu and Fedora; if those
+do not apply to you, you can follow either @ref{Building from a
+release tarball} or @ref{Building from git}.
+
+@subsection Dependencies for Debian/Ubuntu
+
+@example
+$ sudo apt-get install libgmime-3.0-dev libxapian-dev emacs
+@end example
+
+@subsection Dependencies for Fedora
+
+@example
+$ sudo yum install gmime30-devel xapian-core-devel emacs
+@end example
+
+@subsection Building from a release tarball
+@anchor{Building from a release tarball}
+
+Using a release-tarball (as available from
+GitHub@footnote{@url{https://github.com/djcb/mu/releases}}),
+installation follows the typical steps:
+
+@example
+$ tar xvfz mu-<version>.xz # use the specific version
+$ cd mu-<version>
+# On the BSDs: use gmake instead of make
+$ ./configure && make
+$ sudo make install
+@end example
+
+Xapian, GMime and their dependencies must be installed.
+
+@subsection Building from git
+@anchor{Building from git}
+
+By default, @t{mu} uses the
+Meson@footnote{@url{https://mesonbuild.com/}} build-system. For
+ease-of-use, we also provide a @t{Makefile} with some basic options. Of
+course, you can also just use the corresponding @t{meson}/@t{ninja}
+commands directly.
+
+@example
+$ git clone git://github.com/djcb/mu.git
+$ cd mu
+$ ./autogen.sh
+$ make
+$ make install
+@end example
+
+After that, @t{make} (which is just @t{ninja -C build} under the covers)
+should be enough for rebuilding.
+
+Alternatively, you can also use the (now deprecated) @t{autotools} build
+setup, assuming you have autotools (@t{autoconf}, @t{automake},
+@t{libtool}, @t{texinfo}) installed:
+
+@example
+# get from git (alternatively, use a github tarball)
+$ git clone git://github.com/djcb/mu.git
+
+$ cd mu
+$ ./autogen.sh && make
+# On the BSDs: use gmake instead of make
+$ sudo make install
+@end example
+
+(Xapian, GMime and their dependencies must be installed).
+
+After this, @t{mu} and @t{mu4e} should be installed @footnote{there's a
+hard dependency between versions of @t{mu4e} and @t{mu} --- you cannot
+combine different versions} on your system, and be available from the
+command line and in Emacs.
+
+You may need to restart Emacs, so it can find @t{mu4e} in its
+@code{load-path}. If, even after restarting, Emacs cannot find @t{mu4e},
+you may need to add it to your @code{load-path} explicitly; check where
+@t{mu4e} is installed, and add something like the following to your
+configuration before trying again:
+@lisp
+;; the exact path may differ --- check it
+(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e")
+@end lisp
+
+
+@subsection mu4e and emacs customization
+
+There is some support for using the Emacs customization system in
+@t{mu4e}, but for now, we recommend setting the values
+manually. Please refer to @ref{Example configs} for a couple of
+examples of this; here we go through things step-by-step.
+
+@node Getting mail
+@section Getting mail
+
+In order for @t{mu} (and, by extension, @t{mu4e}) to work, you need to
+have your e-mail messages stored in a
+@emph{maildir}@footnote{@url{https://en.wikipedia.org/wiki/Maildir};
+in this manual we use the term `maildir' for both the standard and the
+hierarchy of maildirs that store your messages} --- a specific
+directory structure with one-file-per-message.
+
+If you are already using a maildir, you are lucky. If not, some setup
+is required:
+@itemize
+@item @emph{Using an external IMAP or POP server} --- if you are using an
+@abbr{IMAP} or @abbr{POP} server, you can use tools like @t{getmail},
+@t{fetchmail}, @t{offlineimap} or @t{isync} to download your messages
+into a maildir (@file{~/Maildir}, often). Because it is such a common
+case, there is a full example of setting @t{mu4e} up with
+@t{offlineimap} and Gmail; @pxref{Gmail configuration}.
+@item @emph{Using a local mail server} --- if you are using a local mail- server
+(such as @t{postfix} or @t{qmail}), you can teach them to deliver into
+a maildir as well, maybe in combination with @t{procmail}. A bit of
+googling should be able to provide you with the details.
+@end itemize
+
+While a @t{mu} only supports a single Maildir, it can be spread across
+different file-systems; and symbolic links are supported.
+
+@node Initializing the message store
+@section Initializing the message store
+
+The first time you run @t{mu}, you need to initialize its store
+(database). The default location for that is @t{~/.cache/mu/xapian}, but
+you can change this using the @t{--muhome} option, and remember to pass
+that to the other commands as well. Alternatively, you can use an
+environment variable @t{MUHOME}.
+
+Assuming that your maildir is at @file{~/Maildir}, we issue the
+following command:
+@example
+ $ mu init --maildir=~/Maildir
+@end example
+
+You can add some e-mail addresses, so @t{mu} recognizes them as yours:
+
+@example
+ $ mu init --maildir=~/Maildir --my-address=jim@@example.com --my-address=bob@@example.com
+@end example
+
+@t{mu} remembers the maildir and your addresses and uses them when
+indexing messages. If you want to change them, you need to @t{init}
+once again.
+
+The addresses may also be basic POSIX regular expressions, wrapped in
+slashes, for example:
+
+@example
+ $ mu init --maildir=~/Maildir '--my-address=/foo-.*@@example\.com/'
+@end example
+
+If you want to see the values for your message-store, you can use
+@command{mu info}.
+
+@node Indexing your messages
+@section Indexing your messages
+
+After you have succeeded in @ref{Getting mail} and initialized the
+message database, we need to @emph{index} the messages. That is --- we
+need to scan the messages in the maildir and store the information
+about them in a special database.
+
+We can do that from @t{mu4e} --- @ref{Main view}, but the first time,
+it is a good idea to run it from the command line, which makes it
+easier to verify that everything works correctly.
+
+Assuming that your maildir is at @file{~/Maildir}, we issue the
+following command:
+@example
+ $ mu index
+@end example
+
+This should scan your messages and fill the database, and give
+progress information while doing so.
+
+The indexing process may take a few minutes the first time you do it
+(for thousands of e-mails); afterwards it is much faster, since @t{mu}
+only scans messages that are new or have changed. Indexing is discussed
+in full detail in the @t{mu-index} man-page.
+
+After the indexing process has finished, you can quickly test if
+everything worked, by trying some command-line searches, for example
+@example
+ $ mu find hello
+@end example
+
+which lists all messages that match @t{hello}. For more examples of
+searches, see @ref{Queries}, or check the @t{mu-find} and @t{mu-easy}
+man pages. If all of this worked well, we are well on our way setting
+things up; the next step is to do some basic configuration for @t{mu4e}.
+
+@node Basic configuration
+@section Basic configuration
+
+Before we can start using @t{mu4e}, we need to tell Emacs to load
+it. So, add to your @file{~/.emacs} (or its moral equivalent, such as
+@file{~/.emacs.d/init.el}) something like:
+
+@lisp
+(require 'mu4e)
+@end lisp
+
+If Emacs complains that it cannot find @t{mu4e}, check your
+@code{load-path} and make sure that @t{mu4e}'s installation directory is
+part of it. If not, you can add it:
+
+@lisp
+(add-to-list 'load-path MU4E-PATH)
+@end lisp
+
+with @t{MU4E-PATH} replaced with the actual path.
+
+@node Folders
+@section Folders
+
+The next step is to tell @t{mu4e} where it can find your Maildir, and
+some special folders.
+
+So, for example@footnote{Note that the folders (@t{mu4e-sent-folder},
+@t{mu4e-drafts-folder}, @t{mu4e-trash-folder} and
+@t{mu4e-refile-folder}) can also be @emph{functions} that are evaluated
+at runtime. This allows for dynamically changing them depending on the
+situation. See @ref{Dynamic folders} for details.}:
+@lisp
+;; these are actually the defaults
+(setq
+ mu4e-sent-folder "/sent" ;; folder for sent messages
+ mu4e-drafts-folder "/drafts" ;; unfinished messages
+ mu4e-trash-folder "/trash" ;; trashed messages
+ mu4e-refile-folder "/archive") ;; saved messages
+@end lisp
+
+The folder (maildir) names are all relative to the root-maildir (see the
+output of @command{mu info}). If you use @t{mu4e-context}, see
+@ref{Contexts and special folders} for what that means for these special
+folders.
+
+@node Retrieval and indexing
+@section Retrieval and indexing with mu4e
+
+As we have seen, we can do all of the mail retrieval @emph{outside} of
+Emacs/@t{mu4e}. However, you can also do it from within
+@t{mu4e}.
+
+@subsection Basics
+
+To set up mail-retrieval from within @t{mu4e}, set the variable
+@code{mu4e-get-mail-command} to the program or shell command you want to
+use for retrieving mail. You can then get your e-mail using @kbd{M-x
+mu4e-update-mail-and-index}, or @kbd{C-S-u} in all @t{mu4e}-views;
+alternatively, you can use @kbd{C-c C-u}, which may be more convenient
+if you use emacs in a terminal.
+
+You can kill the (foreground) update process with @kbd{q}.
+
+It is possible to update your mail and index periodically in the
+background or foreground, by setting the variable
+@code{mu4e-update-interval} to the number of seconds between these
+updates. If set to @code{nil}, it won't update at all. After you make
+changes to @code{mu4e-update-interval}, @t{mu4e} must be restarted
+before the changes take effect. By default, this will run in
+background and to change it to run in foreground, set
+@code{mu4e-index-update-in-background} to @code{nil}.
+
+@subsection Handling errors during mail retrieval
+
+If the mail-retrieval process returns with a non-zero exit code,
+@t{mu4e} shows a warning (unless @code{mu4e-index-update-error-warning}
+is set to @code{nil}), but then try to index your maildirs anyway
+(unless @code{mu4e-index-update-error-continue} is set to @code{nil}).
+
+Reason for these defaults is that some of the mail-retrieval programs
+may return non-zero, even when the updating process succeeded; however,
+it is hard to tell such pseudo-errors from real ones like `login
+failed'.
+
+If you need more refinement, it may be useful to wrap the mail-retrieval
+program in a shell-script, for example @t{fetchmail} returns 1 to
+indicate `no mail'; we can handle that with:
+@lisp
+(setq mu4e-get-mail-command "fetchmail -v || [ $? -eq 1 ]")
+@end lisp
+A similar approach can be used with other mail retrieval programs,
+although not all of them have their exit codes documented.
+
+@subsection Implicit mail retrieval
+
+If you don't have a specific command for getting mail, for example
+because you are running your own mail-server, you can leave
+@code{mu4e-get-mail-command} at @t{"true"} (the default), in which case
+@t{mu4e} won't try to get new mail, but still re-index your messages.
+
+@subsection Speeding up indexing
+
+If you have a large number of e-mail messages in your store,
+(re)indexing might take a while. The defaults for indexing are to
+ensure that we always have correct, up-to-date information about your
+messages, even if other programs have modified the Maildir.
+
+The downside of this thoroughness (which is the default) is that it is
+relatively slow, something that can be noticeable with large e-mail
+corpa on slow file-systems. For a faster approach, you can use the
+following:
+
+@lisp
+(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 lisp
+
+In many cases, the mentioned thoroughness might not be needed, and
+these settings give a very significant speed-up. If it does not work
+for you (e.g., @t{mu4e} fails to find some new messages), simply leave
+at the default.
+
+Note that you can occasionally run a thorough indexing round using
+@code{mu4e-update-index-nonlazy}.
+
+For further details, please refer to the @t{mu-index} manpage; in
+particular, see @t{.noindex} and @t{.noupdate} which can help reducing
+the indexing time.
+
+@subsection Example setup
+
+A simple setup could look something like:
+
+@lisp
+(setq
+ mu4e-get-mail-command "offlineimap" ;; or fetchmail, or ...
+ mu4e-update-interval 300) ;; update every 5 minutes
+@end lisp
+
+A hook @code{mu4e-update-pre-hook} is available which is run right
+before starting the process. That can be useful, for example, to
+influence, @code{mu4e-get-mail-command} based on the the current
+situation (location, time of day, ...).
+
+It is possible to get notifications when the indexing process does any
+updates --- for example when receiving new mail. See
+@code{mu4e-index-updated-hook} and some tips on its usage in the
+@ref{FAQ}.
+
+@node Sending mail
+@section Sending mail
+
+@t{mu4e} re-uses Gnus' @code{message-mode} (@ref{Top,,message}) for
+writing mail and inherits the setup for sending mail as well.
+
+For sending mail using @abbr{SMTP}, @t{mu4e} uses @t{smtpmail}
+(@ref{Top,,smtpmail}). This package supports many different ways to
+send mail; please refer to its documentation for the details.
+
+Here, we only provide some simple examples --- for more, see
+@ref{Example configs}.
+
+A very minimal setup:
+
+@lisp
+;; tell message-mode how to send mail
+(setq message-send-mail-function 'smtpmail-send-it)
+;; if our mail server lives at smtp.example.org; if you have a local
+;; mail-server, simply use 'localhost' here.
+(setq smtpmail-smtp-server "smtp.example.org")
+@end lisp
+
+Since @t{mu4e} (re)uses the same @t{message mode} and @t{smtpmail} that
+Gnus uses, many settings for those also apply to @t{mu4e}.
+
+@subsection Dealing with sent messages
+
+By default, @t{mu4e} puts a copy of messages you sent in the folder
+determined by @code{mu4e-sent-folder}. In some cases, this may not be
+what you want - for example, when using Gmail-over-@abbr{IMAP}, this
+interferes with Gmail's handling of the sent messages folder, and you
+may end up with duplicate messages.
+
+You can use the variable @code{mu4e-sent-messages-behavior} to customize
+what happens with sent messages. The default is the symbol @code{sent}
+which, as mentioned, causes the message to be copied to your
+sent-messages folder. Other possible values are the symbols @code{trash}
+(the sent message is moved to the trash-folder
+(@code{mu4e-trash-folder}), and @code{delete} to simply discard the sent
+message altogether (so Gmail can deal with it).
+
+For Gmail-over-@abbr{IMAP}, you could add the following to your
+settings:
+@verbatim
+;; don't save messages to Sent Messages, Gmail/IMAP takes care of this
+(setq mu4e-sent-messages-behavior 'delete)
+@end verbatim
+And that's it! We should now be ready to go.
+
+For more complex needs, @code{mu4e-sent-messages-behavior} can also be
+a parameter-less function that returns one of the mentioned symbols;
+see the built-in documentation for the variable.
+
+@node Running mu4e
+@section Running mu4e
+
+After following the steps in this chapter, we now (hopefully!) have a
+working @t{mu4e} setup. Great! In the next chapters, we walk you
+through the various views in @t{mu4e}.
+
+For your orientation, the diagram below shows how the views relate to each
+other, and the default key-bindings to navigate between them.
+
+@cartouche
+@verbatim
+
+ [C] +--------+ [RFCE]
+ --------> | editor | <--------
+ / +--------+ \
+ / [RFCE]^ \
+/ | \
++-------+ [sjbB]+---------+ [RET] +---------+
+| main | <---> | headers | <----> | message |
++-------+ [q] +---------+ [qbBjs] +---------+
+ [sjbB] ^
+[.] | [q]
+ V
+ +-----+
+ | raw |
+ +-----+
+
+Default bindings
+----------------
+R: Reply s: search .: raw view (toggle)
+F: Forward j: jump-to-maildir q: quit
+C: Compose b: bookmark-search
+E: Edit B: edit bookmark-search
+
+@end verbatim
+@end cartouche
+
+@node Main view
+@chapter The main view
+
+After you have installed @t{mu4e} (@pxref{Getting started}), you can start it
+with @kbd{M-x mu4e}. @t{mu4e} does some checks to ensure everything is set up
+correctly, and then shows you the @t{mu4e} main view. Its major mode is
+@code{mu4e-main-mode}.
+
+@menu
+* Overview: MV Overview. What is the main view
+* Basic actions::What can we do
+* Bookmarks: MV Bookmarks. Jumping to other places
+* Miscellaneous::Notes
+@end menu
+
+@node MV Overview
+@section Overview
+
+The main view looks something like the following:
+
+@cartouche
+@verbatim
+* mu4e - mu for emacs version x.y.z
+
+ Basics
+
+ * [j]ump to some maildir
+ * enter a [s]earch query
+ * [C]ompose a new message
+
+ Bookmarks
+
+ * [bu] Unread messages (13085/13085)
+ * [bt] Today's messages
+ * [bw] Last 7 days (53/128)
+ * [bp] Messages with images (75/2441)
+
+ Maildirs
+
+ * [ja] /archive (2101/18837)
+ * [ji] /inbox (1/2)
+ * [jb] /bulk (33/35)
+ * [jB] /bulkarchive (179/2090)
+ * [jm] /mu (694/17687)
+ * [jn] /sauron
+ * [js] /sent
+
+ Misc
+
+ * [;]Switch context
+ * [U]pdate email & database
+ * toggle [m]ail sending mode (currently direct)
+ * [f]lush 1 queued mail
+
+ * [N]ews
+ * [A]bout mu4e
+ * [H]elp
+ * [q]uit
+
+ Info
+
+ * last updated : Sat May 7 20:37:37 2022
+ * database-path : /home/pam/.cache/mu/xapian
+ * maildir : /home/pam/Maildir
+ * in store : 86179 messages
+ * personal addresses : /.*example.com/, pam@fo
+
+@end verbatim
+@end cartouche
+
+Let's walk through the menu.
+
+@node Basic actions
+@section Basic actions
+
+First, the @emph{Basics}:
+@itemize
+@item @t{[j]ump to some maildir}: after pressing @key{j} (``jump''),
+@t{mu4e} asks you for a maildir to visit. These are the maildirs you
+set in @ref{Basic configuration} and any of your own. If you choose
+@key{o} (``other'') or @key{/}, you can choose from all maildirs under
+the root-maildir. After choosing a maildir, the messages in that
+maildir are listed, in the @ref{Headers view}.
+@item @t{enter a [s]earch query}: after pressing @key{s}, @t{mu4e} asks
+you for a search query, and after entering one, shows the results in the
+@ref{Headers view}.
+@item @t{[C]ompose a new message}: after pressing @key{C}, you are dropped in
+the @ref{Editor view} to write a new message.
+@end itemize
+
+@node MV Bookmarks
+@section Bookmarks
+
+The next item in the Main view is @emph{Bookmarks}.
+
+Bookmarks are predefined queries with a descriptive name and a
+shortcut --- in the example above, we see the default bookmarks. You
+can view the list of messages matching a certain bookmark by pressing
+@key{b} followed by the bookmark's shortcut. If you'd like to edit the
+bookmarked query first before invoking it, use @key{B}.
+
+Next to each bookmark there is the number of (unread/all) messages
+that match.
+
+Bookmarks are stored in the variable @code{mu4e-bookmarks}; you can add
+your own and/or replace the default ones; @xref{Bookmarks}. For
+instance:
+@lisp
+(add-to-list 'mu4e-bookmarks
+ ;; add bookmark for recent messages on the Mu mailing list.
+ '( :name "Mu7Days"
+ :key ?m
+ :query "list:mu-discuss.googlegroups.com AND date:7d..now"))
+@end lisp
+
+There are optional keys @t{:hide} to hide the bookmark from the main
+menu, but still have it available (using @key{b})) and
+@t{:hide-unread} to avoid generating the unread-number; that can be
+useful if you have bookmarks for slow queries. Note that
+@t{:hide-unread} is implied when the query is not a string; this for
+the common case where the query function involves some user input,
+which would be disruptive in this case.
+
+@node Miscellaneous
+@section Miscellaneous
+
+Finally, there are some @emph{Misc} (miscellaneous) actions:
+@itemize
+@item @t{[U]pdate email & database} executes the shell-command in the variable
+@code{mu4e-get-mail-command}, and afterwards updates the @t{mu}
+database; see @ref{Indexing your messages} and @ref{Getting mail} for
+details.
+@item @t{toggle [m]ail sending mode (direct)} toggles between sending
+mail directly, and queuing it first (for example, when you are offline),
+and @t{[f]lush queued mail} flushes any queued mail. This item is
+visible only if you have actually set up mail-queuing. @ref{Queuing
+mail}
+@item @t{[A]bout mu4e} provides general information about the program
+@item @t{[H]elp} shows help information for this view
+@item Finally, @t{[q]uit mu4e} quits your @t{mu4e}-session
+@end itemize
+
+@node Headers view
+@chapter The headers view
+
+The headers view shows the results of a query. The header-line shows
+the names of the fields. Below that, there is a line with those
+fields, for each matching message, followed by a footer line. The
+major-mode for the headers view is @code{mu4e-headers-mode}.
+
+@menu
+* Overview: HV Overview. What is the Header View
+* Keybindings::Do things with your keyboard
+* Marking: HV Marking. Selecting messages for doing things
+* Sorting and threading::Influencing the display
+* Custom headers: HV Custom headers. Adding your own headers
+* Actions: HV Actions. Defining and using actions
+* Split view::Seeing both headers and messages
+@end menu
+
+@node HV Overview
+@section Overview
+
+An example headers view:
+@cartouche
+@verbatim
+Date V Flgs From/To List Subject
+06:32 Nu To Edmund Dantès GstDev Gstreamer-V4L2SINK ...
+15:08 Nu Abbé Busoni GstDev ├> ...
+18:20 Nu Pierre Morrel GstDev │└> ...
+07:48 Nu To Edmund Dantès GstDev └> ...
+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 verbatim
+@end cartouche
+
+Some notes to explain what you see in the example:
+
+@itemize
+@item The fields shown in the headers view can be influenced by customizing
+the variable @code{mu4e-headers-fields}; see @code{mu4e-header-info} for
+the list of built-in fields. Apart from the built-in fields, you can
+also create custom fields using @code{mu4e-header-info-custom}; see
+@ref{HV Custom headers} for details.
+@item By default, the date is shown with the @t{:human-date} field, which
+shows the @emph{time} for today's messages, and the @emph{date} for
+older messages. If you do not want to distinguish between `today' and
+`older', you can use the @t{:date} field instead.
+@item You can customize the date and time formats with the variable
+@code{mu4e-headers-date-format} and @code{mu4e-headers-time-format},
+respectively. In the example, we use @code{:human-date}, which shows the
+time when the message was sent today, and the date otherwise.
+@item By default, the subject is shown using the @t{:subject} field;
+however, it is also possible to use @t{:thread-subject}, which shows
+the subject of a thread only once, similar to the display of the
+@t{mutt} e-mail client.
+@item The header field used for sorting is indicated by ``@t{V}'' or
+``@t{^}''@footnote{or you can use little graphical triangles; see
+variable @code{mu4e-use-fancy-chars}}, corresponding to the sort order
+(descending or ascending, respectively). You can influence this by a
+mouse click, or @key{O}. Not all fields allow sorting.
+@item Instead of showing the @t{From:} and @t{To:} fields separately, you
+can use From/To (@t{:from-or-to} in @code{mu4e-headers-fields} as a more
+compact way to convey the most important information: it shows @t{From:}
+@emph{except} when the e-mail was sent by the user (i.e., you) --- in
+that case it shows @t{To:} (prefixed by @t{To}@footnote{You can
+customize this by changing the variable
+@code{mu4e-headers-from-or-to-prefix} (a cons cell)}, as in the example
+above).
+@item The `List' field shows the mailing-list a message is sent to;
+@code{mu4e} tries to create a convenient shortcut for the mailing-list
+name; the variable @code{mu4e-user-mailing-lists} can be used to add
+your own shortcuts. You can use @code{mu4e-mailing-list-patterns} to
+specify generic shortcuts. For instance, to shorten list names to the
+part before @t{-list}, you could use:
+@lisp
+(setq mu4e-mailing-list-patterns '("\\`\\([-_a-z0-9.]+\\)-list"))
+@end lisp
+@item The letters in the `Flags' field correspond to the following: D=@emph{draft},
+F=@emph{flagged} (i.e., `starred'), N=@emph{new}, P=@emph{passed} (i.e.,
+forwarded), R=@emph{replied}, S=@emph{seen}, T=@emph{trashed},
+a=@emph{has-attachment}, x=@emph{encrypted}, s=@emph{signed},
+u=@emph{unread}. The tooltip for this field also contains this information.
+@item The subject field also indicates the discussion threads @footnote{using
+Jamie Zawinski's mail threading algorithm,
+@url{https://www.jwz.org/doc/threading.html}}.
+@item The headers view is @emph{automatically updated} if any changes are
+found during the indexing process, and if there is no current
+user-interaction. If you do not want such automatic updates, set
+@code{mu4e-headers-auto-update} to @code{nil}.
+@item Just before executing a search, a hook-function
+@code{mu4e-headers-search-hook} is invoked, which receives the search
+expression as its parameter.
+@item Also, there is a hook-function @code{mu4e-headers-found-hook} available which
+is invoked just after @t{mu4e} has completed showing the messages in the
+headers-view.
+@end itemize
+
+@node Keybindings
+@section Keybindings
+
+Using the below key bindings, you can do various things with these
+messages; these actions are also listed in the @t{Mu4e} menu in the
+Emacs menu bar.
+
+@verbatim
+key description
+===========================================================
+n,p view the next, previous message
+],[ move to the next, previous unread message
+y select the message view (if it's visible)
+RET open the message at point in the message view
+
+searching
+---------
+s search
+S edit last query
+/ narrow the search
+b search bookmark
+B edit bookmark before search
+j jump to maildir
+M-left,\ previous query
+M-right next query
+
+O change sort order
+P toggle threading
+Q toggle full-search
+V toggle skip-duplicates
+W toggle include-related
+
+marking
+-------
+d mark for moving to the trash folder
+= mark for removing trash flag ('untrash')
+DEL,D mark for complete deletion
+m mark for moving to another maildir folder
+r mark for refiling
++,- mark for flagging/unflagging
+?,! mark message as unread, read
+
+u unmark message at point
+U unmark *all* messages
+
+% mark based on a regular expression
+T,t mark whole thread, subthread
+
+<insert>,* mark for 'something' (decide later)
+# resolve deferred 'something' marks
+
+x execute actions for the marked messages
+
+composition
+-----------
+R,F,C reply/forward/compose
+E edit (only allowed for draft messages)
+
+
+misc
+----
+a execute some custom action on a header
+| pipe message through shell command
+C-+,C-- increase / decrease the number of headers shown
+H get help
+C-S-u update mail & reindex
+q leave the headers buffer
+@end verbatim
+
+Furthermore, a number of keybindings are available through minor modes:
+@itemize
+@item Context; see @pxref{Contexts}.
+@end itemize
+
+@node HV Marking
+@section Marking
+
+You can @emph{mark} messages for a certain action, such as deletion or
+move. After one or more messages are marked, you can then execute
+(@code{mu4e-mark-execute-all}, @key{x}) these actions. This two-step
+mark-execute sequence is similar to what e.g. @t{dired} does. It is how
+@t{mu4e} tries to be as quick as possible, while avoiding accidents.
+
+The mark/unmark commands support the @emph{region} (i.e., ``selection'')
+--- so, for example, if you select some messages and press @key{DEL},
+all messages in the region are marked for deletion.
+
+You can mark all messages that match a certain pattern with @key{%}. In
+addition, you can mark all messages in the current thread (@key{T}) or
+sub-thread (@key{t}).
+
+When you do a new search or refresh the headers buffer while you still
+have marked messages, you are asked what to do with those marks ---
+whether to @emph{apply} them before leaving, or @emph{ignore} them. This
+behavior can be influenced with the variable
+@code{mu4e-headers-leave-behavior}.
+
+For more information about marking, see @ref{Marking}.
+
+@node Sorting and threading
+@section Sorting and threading
+
+By default, @t{mu4e} sorts messages by date, in descending order: the
+most recent messages are shown at the top. In addition, be default
+@t{mu4e} shows the message @emph{threads}, i.e., the tree structure
+representing a discussion thread; this also affects the sort order:
+the top-level messages are sorted by the date of the @emph{newest}
+message in the thread.
+
+The header field used for sorting is indicated by ``@t{V}'' or
+``@t{^}''@footnote{or you can use little graphical triangles; see
+variable @code{mu4e-use-fancy-chars}}, indicating the sort order
+(descending or ascending, respectively).
+
+You can change the sort order by clicking the corresponding column with
+the mouse, or with @kbd{M-x mu4e-headers-change-sorting} (@key{O}); note
+that not all fields can be used for sorting. You can toggle threading
+on/off using @kbd{M-x mu4e-headers-toggle-threading} or @key{P}. For
+both of these functions, unless you provide a prefix argument
+(@key{C-u}), the current search is updated immediately using the new
+parameters. You can toggle full-search (@ref{Searching}) using @kbd{M-x
+mu4e-headers-toggle-full-search} or @key{Q}.
+
+Note that with threading enabled, the sorting is exclusively by date,
+regardless of the column clicked.
+
+If you want to change the defaults for these settings, you can use the
+variables @code{mu4e-headers-sort-field} and
+@code{mu4e-headers-show-threads}, as well as
+@code{mu4e-headers-change-sorting} to change the sorting of the current
+search results.
+
+@node HV Custom headers
+@section Custom headers
+
+Sometimes the normal headers that @t{mu4e} offers (Date, From, To,
+Subject, etc.)@: may not be enough. For these cases, @t{mu4e} offers
+@emph{custom headers} in both the headers-view and the message-view.
+
+You can do so by adding a description of your custom header to
+@code{mu4e-header-info-custom}, which is a list of custom headers.
+
+Let's look at an example --- suppose we want to add a custom header that
+shows the number of recipients for a message, i.e., the sum of the
+number of recipients in the @t{To:} and @t{Cc:} fields. Let's further
+suppose that our function takes a message-plist as its argument
+(@ref{Message functions}).
+
+@lisp
+(add-to-list 'mu4e-header-info-custom
+ '(:recipnum .
+ ( :name "Number of recipients" ;; long name, as seen in the message-view
+ :shortname "Recip#" ;; short name, as seen in the headers view
+ :help "Number of recipients for this message" ;; tooltip
+ :function (lambda (msg)
+ (format "%d"
+ (+ (length (mu4e-message-field msg :to))
+ (length (mu4e-message-field msg :cc))))))))
+@end lisp
+
+Or, let's get the full mailing-list name:
+@lisp
+(add-to-list 'mu4e-header-info-custom
+ '(:full-mailing-list .
+ ( :name "Mailing-list" ;; long name, as seen in the message-view
+ :shortname "ML" ;; short name, as seen in the headers view
+ :help "Full name for mailing list" ;; tooltip
+ :function (lambda (msg)
+ (or (mu4e-message-field msg :mailing-list) "")))))
+@end lisp
+
+You can then add the custom header to your @code{mu4e-headers-fields},
+just like the built-in headers. After evaluation, your headers-view
+should include a new header @t{Recip#} with the number of recipients,
+and/or @t{ML} with the full mailing-list name.
+
+This function can be used in both the headers-view and the message-view;
+if you need something specific for one of these, you can check for the
+mode in your function, or create separate functions.
+
+@node HV Actions
+@section Actions
+
+@code{mu4e-headers-action} (@key{a}) lets you pick custom actions to perform
+on the message at point. You can specify these actions using the variable
+@code{mu4e-headers-actions}. See @ref{Actions} for the details.
+
+@t{mu4e} defines some default actions. One of those is for @emph{capturing} a
+message: @key{a c} `captures' the current message. Next, when you're editing
+some message, you can include the previously captured message as an
+attachment, using @code{mu4e-compose-attach-captured-message}. See
+@file{mu4e-actions.el} in the @t{mu4e} source distribution for more example
+actions.
+
+@node Split view
+@section Split view
+
+Using the @emph{Split view}, we can see the @ref{Headers view} and the
+@ref{Message view} next to each other, with the message selected in the
+former, visible in the latter. You can influence the way the splitting
+is done by customizing the variable @code{mu4e-split-view}. Possible
+values are:
+
+@itemize
+@item @t{horizontal} (this is the default): display the message view below the
+header view. Use @code{mu4e-headers-visible-lines} the set the number of
+lines shown (default: 8).
+@item @t{vertical}: display the message view on the
+right side of the header view. Use @code{mu4e-headers-visible-columns} to set
+the number of visible columns (default: 30).
+@item @t{single-window}: single window mode. Single-window mode tries to
+minimize mu4e window operations (opening, killing, resizing, etc) and
+buffer changes, while still retaining the view and headers buffers. In
+addition, it replaces mu4e main view with a minibuffer prompt containing
+the same information.
+@item anything else: don't do any splitting
+@end itemize
+
+@noindent
+Some useful key bindings in the split view:
+@itemize
+@item @key{C-+} and @key{C--}: interactively change the number of columns or
+headers shown
+@item You can change the selected window from the
+headers-view to the message-view and vice-versa with
+@code{mu4e-select-other-view}, bound to @key{y}
+@end itemize
+
+
+@node Message view
+@chapter The message view
+
+This chapter discusses the message view; this is new (since version
+1.6) message view, based on Gnus' Article Mode, which replaces the
+older one.
+
+After selecting a message in the @ref{Headers view}, it appears in a
+message view window, which shows the message headers, followed by the
+message body. Its major mode is @code{mu4e-view-mode}, which derives
+from @t{gnus-article-mode}.
+
+@menu
+* Overview: MSGV Overview. What is the Message View
+* Keybindings: MSGV Keybindings. Do things with your keyboard
+* Rich-text and images: MSGV Rich-text and images. Reading rich-text messages
+* Custom headers: MSGV Custom headers. Your very own headers
+* Actions: MSGV Actions. Defining and using actions.
+@end menu
+
+@node MSGV Overview
+@section Overview
+
+An example message view:
+
+@cartouche
+@verbatim
+ From: randy@epiphyte.com
+ To: julia@eruditorum.org
+ Subject: Re: some pics
+ Flags: seen, attach
+ Date: Thu, 11 Feb 2021 12:59:30 +0200 (4 weeks, 3 days, 21 hours ago)
+ Maildir: /inbox
+ Attachments: [2. image/jpeg; DSCN4961.JPG]... [3. image/jpeg; DSCN4962.JPG]...
+
+ Hi Julia,
+
+ Some pics from our trip to Cerin Amroth. Enjoy!
+
+ All the best,
+ Randy.
+
+ On Sun 21 Dec 2003 09:06:34 PM EET, Julia wrote:
+
+ [....]
+@end verbatim
+@end cartouche
+
+Some notes:
+@itemize
+@item The variable @code{mu4e-view-fields} determines the header fields to be
+shown; see @code{mu4e-header-info} for a list of built-in fields. Apart
+from the built-in fields, you can also create custom fields using
+@code{mu4e-header-info-custom}; see @ref{MSGV Custom headers}.
+@item For search-related operations, see @ref{Searching}.
+@item You can scroll down the message using @key{SPC}; if you do this at the
+end of a message,it automatically takes you to the next one. If you want
+to prevent this behavior, set @code{mu4e-view-scroll-to-next} to
+@code{nil}.
+@end itemize
+
+@node MSGV Keybindings
+@section Keybindings
+
+You can find most things you can do with this message in the @emph{Mu4e}
+menu, or by using the keyboard; the default bindings are:
+
+@verbatim
+key description
+==============================================================
+n,p view the next, previous message
+],[ move to the next, previous unread message
+y select the headers view (if it's visible)
+
+RET scroll down
+M-RET open URL at point / attachment at point
+
+SPC scroll down, if at end, move to next message
+S-SPC scroll up
+
+searching
+---------
+s search
+S edit last query
+/ narrow the search
+b search bookmark
+B edit bookmark before search
+j jump to maildir
+
+M-left previous query
+M-right next query
+
+marking messages
+----------------
+d mark for moving to the trash folder
+= mark for removing trash flag ('untrash')
+DEL,D mark for complete deletion
+m mark for moving to another maildir folder
+r mark for refiling
++,- mark for flagging/unflagging
+
+u unmark message at point
+U unmark *all* messages
+
+% mark based on a regular expression
+T,t mark whole thread, subthread
+
+<insert>,* mark for 'something' (decide later)
+# resolve deferred 'something' marks
+
+x execute actions for the marked messages
+
+composition
+-----------
+R,F,C reply/forward/compose
+E edit (only allowed for draft messages)
+
+actions
+-------
+g go to (visit) numbered URL (using `browse-url')
+(or: <mouse-1> or M-RET with point on url)
+C-u g visits multiple URLs
+f fetch (download )the numbered URL.
+C-u f fetches multiple URLs
+k save the numbered URL in the kill-ring.
+C-u k saves multiple URLs
+
+e extract (save) one or more attachments (asks for numbers)
+(or: <mouse-2> or S-RET with point on attachment)
+a execute some custom action on the message
+A execute some custom action on the message's MIME-parts
+
+misc
+----
+. show the raw message view. 'q' takes you back.
+C-+,C-- increase / decrease the number of headers shown
+H get help
+C-S-u update mail & reindex
+q leave the message view
+@end verbatim
+
+Furthermore, a number of keybindings are available through minor modes:
+@itemize
+@item Context; see @pxref{Contexts}.
+@end itemize
+
+For the marking commands, please refer to @ref{Marking messages}.
+
+@node MSGV Rich-text and images
+@section Reading rich-text messages
+
+These days, many e-mail messages contain rich-text (typically, HTML);
+either as an alternative to a text-only version, or even as the only
+option.
+
+By default, mu4e tries to display the 'richest' option, which is the
+last MIME-part of the alternatives. You can customize this to prefer
+the text version, if available, with something like the following in
+your configuration (and see the docstring for
+@t{mm-discouraged-alternatives} for details):
+
+@lisp
+(with-eval-after-load "mm-decode"
+ (add-to-list 'mm-discouraged-alternatives "text/html")
+ (add-to-list 'mm-discouraged-alternatives "text/richtext"))
+@end lisp
+
+When displaying rich-text messages inline, @t{mu4e} (through @t{gnus})
+uses the @t{shr} built-in HTML-renderer. If you're using a dark color
+theme, and the messages are hard to read, it can help to change the
+luminosity, e.g.:
+@lisp
+(setq shr-color-visible-luminance-min 80)
+@end lisp
+
+Note that you can switch between the HTML and text versions by
+clicking on the relevant part in the messages headers; you can make it
+even clearer by indicating them in the message itself, using:
+
+@lisp
+(setq gnus-unbuttonized-mime-types nil)
+@end lisp
+
+@subsection Inline images
+When you run Emacs in graphical mode, by default images attached to
+messages are shown inline in the message view buffer.
+
+To disable this, set @code{gnus-inhibit-images} to @t{t}. By default,
+external images in HTML are not retrieved from external URLs because
+they can be used to track you.
+
+Apart from that, you can also control whether to load remote images;
+since loading remote images is often used for privacy violations, by
+default this is not allowed.
+
+You can specify what URLs to block by setting
+@code{gnus-blocked-images} to a regular expression or to a function
+that will receive a single parameter which is not meaningful for
+@t{mu4e}.
+
+For example, to enable images in Github notifications, you could use
+the following:
+
+@lisp
+(setq gnus-blocked-images
+ (lambda(&optional _ignore)
+ (if (mu4e-message-contact-field-matches
+ (mu4e-message-at-point) :from "notifications@@github.com")
+ nil ".")))
+@end lisp
+
+@code{mu4e} inherits the default @t{gnus-blocked-images} from Gnus and
+ensures that it works with @t{mu4e} too. However, mu4e is not Gnus, so
+if you have Gnus-specific settings for @t{gnus-blocked-images}, you
+should verify that they have the desired effect in @code{mu4e} as
+well.
+
+@node MSGV Custom headers
+@section Custom headers
+
+Sometimes the normal headers (Date, From, To, Subject, etc.)@: may not be
+enough. For these cases, @t{mu4e} offers @emph{custom headers} in both
+the headers-view and the message-view.
+
+See @ref{HV Custom headers} for an example of this; the difference for
+the message-view is that you should add your custom header to
+@code{mu4e-view-fields} rather than @code{mu4e-headers-fields}.
+
+@node MSGV Actions
+@section Actions
+
+You can perform custom functions (``actions'') on messages and their
+attachments. For a general discussion on how to define your own, see
+@ref{Actions}.
+
+@subsection Message actions
+@code{mu4e-view-action} (@key{a}) lets you pick some custom action to perform
+on the current message. You can specify these actions using the variable
+@code{mu4e-view-actions}; @t{mu4e} defines a number of example actions.
+
+@subsection MIME-part actions
+MIME-part actions allow you to act upon MIME-parts in a message - such
+as attachments. For now, these actions are defined and documented in
+@code{mu4e-view-mime-part-actions}.
+
+
+@node Editor view
+@chapter The editor view
+
+Writing e-mail messages takes place in the Editor View. @t{mu4e}'s editor view
+builds on top of Gnus' @t{message-mode}. Most of the @t{message-mode}
+functionality is available, as well some @t{mu4e}-specifics. Its major mode is
+@code{mu4e-compose-mode}.
+
+@menu
+* Overview: EV Overview. What is the Editor view
+* Keybindings: EV Keybindings. Doing things with your keyboard
+* Address autocompletion:: Quickly entering known addresses
+* Compose hooks::Calling functions when composing
+* Signing and encrypting:: Support for cryptography
+* Queuing mail:: Sending mail when the time is ripe
+* Message signatures:: Adding your personal footer to messages
+* Other settings::Miscellanea
+@end menu
+
+@node EV Overview
+@section Overview
+
+@cartouche
+@verbatim
+ From: Rupert the Monkey <rupert@example.com>
+ To: Wally the Walrus <wally@example.com>
+ Subject: Re: Eau-qui d'eau qui?
+ --text follows this line--
+
+ On Mon 16 Jan 2012 10:18:47 AM EET, Wally the Walrus wrote:
+
+ > Hi Rupert,
+ >
+ > Dude - how are things?
+ >
+ > Later -- wally.
+@end verbatim
+@end cartouche
+
+@node EV Keybindings
+@section Keybindings
+
+@t{mu4e}'s editor view derives from Gnus' message editor and shares most of
+its keybindings. Here are some of the more useful ones (you can use the menu
+to find more):
+
+@verbatim
+key description
+--- -----------
+C-c C-c send message
+C-c C-d save to drafts and leave
+C-c C-k kill the message buffer (the message remains in the draft folder)
+C-c C-a attach a file (pro-tip: drag & drop works as well)
+C-c C-; switch the context
+
+(mu4e-specific)
+C-S-u update mail & reindex
+@end verbatim
+
+@node Address autocompletion
+@section Address autocompletion
+
+@t{mu4e} supports@footnote{GNU Emacs 24.4 or higher is required}
+autocompleting addresses when composing e-mail messages. @t{mu4e} uses the
+e-mail addresses from the messages you sent or received as the source for
+this. Address auto-completion is enabled by default; if you want to disable it
+for some reason, set @t{mu4e-compose-complete-addresses} to @t{nil}.
+
+Emacs 24 also supports cycling through the alternatives. When there are more
+than @emph{5} matching addresses, they are shown in a @t{*Completions*}
+buffer. Once the number of matches gets below this number, one is inserted in
+the address field and you can cycle through the alternatives using @key{TAB}.
+
+@subsection Limiting the number of addresses
+
+If you have a lot of mail, especially from mailing lists and the like, there
+can be a @emph{lot} of e-mail addresses, many of which may not be very useful
+when auto-completing. For this reason, @t{mu4e} attempts to limit the number
+of e-mail addresses in the completion pool by filtering out the ones that are
+not likely to be relevant. The following variables are available for tuning
+this:
+
+@itemize
+@item @code{mu4e-compose-complete-only-personal} --- when set to @t{t},
+only consider addresses that were seen in @emph{personal} messages ---
+that is, messages in which one of my e-mail addresses was seen in one
+of the address fields. This is to exclude mailing list posts. You can
+define what is considered `my e-mail address' using the
+@t{--my-address} parameter to @t{mu init}.
+
+@item @code{mu4e-compose-complete-only-after} --- only consider e-mail
+addresses last seen after some date. Parameter is a string, parseable by
+@code{org-parse-time-string}. This excludes old e-mail addresses. The
+default is @t{"2010-01-01"}, i.e., only consider e-mail addresses seen
+since the start of 2010.
+@item @code{mu4e-compose-complete-max} -- the maximum number of contacts to use. This adds a hard limit to the 2000 (default) contacts; those are sorted by recency /frequency etc. so should include the ones you most likely need.
+@item @code{mu4e-contact-process-function} --- a function to rewrite or
+exclude certain addresses.
+@end itemize
+
+@node Compose hooks
+@section Compose hooks
+
+If you want to change some setting, or execute some custom action before
+message composition starts, you can define a @emph{hook function}. @t{mu4e}
+offers two hooks:
+@itemize
+@item @code{mu4e-compose-pre-hook}: this hook is run @emph{before} composition
+starts; if you are composing a @emph{reply}, @emph{forward} a message, or
+@emph{edit} an existing message, the variable
+@code{mu4e-compose-parent-message} points to the message being replied to,
+forwarded or edited, and you can use @code{mu4e-message-field} to get the
+value of various properties (and see @ref{Message functions}).
+@item @code{mu4e-compose-mode-hook}: this hook is run just before composition
+starts, when the whole buffer has already been set up. This is a good place
+for editing-related settings. @code{mu4e-compose-parent-message} (see above)
+is also at your disposal.
+@end itemize
+
+@noindent
+Let's look at some examples. First, suppose we want to set the
+@t{From:}-address for a reply message based on the receiver of the original:
+@lisp
+;; 1) messages to me@@foo.example.com should be replied with From:me@@foo.example.com
+;; 2) messages to me@@bar.example.com should be replied with From:me@@bar.example.com
+;; 3) all other mail should use From:me@@cuux.example.com
+(add-hook 'mu4e-compose-pre-hook
+ (defun my-set-from-address ()
+ "Set the From address based on the To address of the original."
+ (let ((msg mu4e-compose-parent-message)) ;; msg is shorter...
+ (when msg
+ (setq user-mail-address
+ (cond
+ ((mu4e-message-contact-field-matches msg :to "me@@foo.example.com")
+ "me@@foo.example.com")
+ ((mu4e-message-contact-field-matches msg :to "me@@bar.example.com")
+ "me@@bar.example.com")
+ (t "me@@cuux.example.com")))))))
+@end lisp
+
+Secondly, as mentioned, @code{mu4e-compose-mode-hook} is especially
+useful for editing-related settings. For example:
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+ (defun my-do-compose-stuff ()
+ "My settings for message composition."
+ (set-fill-column 72)
+ (flyspell-mode)))
+@end lisp
+
+This hook is also useful for adding headers or changing headers, since the
+message is fully formed when this hook runs. For example, to add a
+@t{Bcc:}-header, you could add something like the following, using
+@code{message-add-header} from @code{message-mode}.
+
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+ (defun my-add-bcc ()
+ "Add a Bcc: header."
+ (save-excursion (message-add-header "Bcc: me@@example.com\n"))))
+@end lisp
+
+Or to something context-specific:
+
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+ (lambda()
+ (let* ((ctx (mu4e-context-current))
+ (name (if ctx (mu4e-context-name ctx))))
+ (when name
+ (cond
+ ((string= name "account1")
+ (save-excursion (message-add-header "Bcc: account1@@example.com\n")))
+ ((string= name "account2")
+ (save-excursion (message-add-header "Bcc: account2@@example.com\n"))))))))
+@end lisp
+
+@noindent
+For a more general discussion about extending @t{mu4e}, see @ref{Extending
+mu4e}.
+
+@node Signing and encrypting
+@section Signing and encrypting
+
+Signing and encrypting of messages is possible using @t{emacs-mime},
+most easily accessed through the @t{Attachments}-menu while composing a
+message, or with @kbd{M-x mml-secure-message-encrypt-pgp}, @kbd{M-x
+mml-secure-message-sign-pgp}.
+
+Important note: the messages are encrypted when they are @emph{sent}:
+this means that draft messages are @emph{not} encrypted. So if you are
+using e.g. @t{offlineimap} or @t{mbsync} to synchronize with some
+remote IMAP-service, make sure the drafts folder is @emph{not} in the
+set of synchronized folders, for obvious reasons.
+
+@node Queuing mail
+@section Queuing mail
+
+If you cannot send mail right now, for example because you are
+currently offline, you can @emph{queue} the mail, and send it when you
+have restored your internet connection. You can control this from the
+@ref{Main view}.
+
+To allow for queuing, you need to tell @t{smtpmail} where you want to store
+the queued messages. For example:
+
+@lisp
+(setq smtpmail-queue-mail t ;; start in queuing mode
+ smtpmail-queue-dir "~/Maildir/queue/cur")
+@end lisp
+
+For convenience, we put the queue directory somewhere in our normal
+maildir. If you want to use queued mail, you should create this directory
+before starting @t{mu4e}. The @command{mu mkdir} command may be useful here,
+so for example:
+
+@verbatim
+ $ mu mkdir ~/Maildir/queue
+ $ touch ~/Maildir/queue/.noindex
+@end verbatim
+
+The file created by the @command{touch} command tells @t{mu} to ignore this
+directory for indexing, which makes sense since it contains @t{smtpmail}
+meta-data rather than normal messages; see the @t{mu-mkdir} and @t{mu-index}
+man-pages for details.
+
+@emph{Warning}: when you switch on queued-mode, your messages @emph{won't}
+reach their destination until you switch it off again; so, be careful not to
+do this accidentally!
+
+@node Message signatures
+@section Message signatures
+
+Message signatures are the standard footer blobs in e-mail messages where you
+can put in information you want to include in every message. The text to
+include is set with @code{mu4e-compose-signature}.
+
+If you don't want to include this automatically with each message,
+you can set @code{mu4e-compose-signature-auto-include} to @code{nil}; you can
+then still include the signature manually, using the function
+@code{message-insert-signature}, typically bound to @kbd{C-c C-w}.
+
+@node Other settings
+@section Other settings
+
+@itemize
+@item If you want use @t{mu4e} as Emacs' default program for sending mail,
+see @ref{Emacs default}.
+@item Normally, @t{mu4e} @emph{buries} the message buffer after sending; if you want
+to kill the buffer instead, add something like the following to your
+configuration:
+@lisp
+(setq message-kill-buffer-on-exit t)
+@end lisp
+@item If you want to exclude your own e-mail address when ``replying to
+all'', set @code{mu4e-compose-dont-reply-to-self} to @code{t}. In
+order for this to work properly you need to pass your address to
+@command{mu init --my-address=} at database initialization time.
+@end itemize
+
+@node Searching
+@chapter Searching
+
+@t{mu4e} is fully search-based: even if you `jump to a folder', you are
+executing a query for messages that happen to have the property of being in a
+certain folder (maildir).
+
+Normally, queries return up to @code{mu4e-headers-results-limit} (default:
+500) results. That is usually more than enough, and makes things significantly
+faster. Sometimes, however, you may want to show @emph{all} results; you can
+enable this with @kbd{M-x mu4e-headers-toggle-full-search}, or by customizing
+the variable @code{mu4e-headers-full-search}. This applies to all search
+commands.
+
+You can also influence the sort order and whether threads are shown or not;
+see @ref{Sorting and threading}.
+
+@menu
+* Queries:: Searching for messages.
+* Bookmarks:: Remembering queries.
+* Maildir searches:: Queries for maildirs.
+* Other search functionality:: Some more tricks.
+@end menu
+
+@node Queries
+@section Queries
+
+@t{mu4e} queries are the same as the ones that @t{mu find}
+understands@footnote{with the caveat that command-line queries are
+subject to the shell's interpretation before @t{mu} sees them}. You can
+consult the @code{mu-query} man page for the details.
+
+Additionally, @t{mu4e} supports @kbd{TAB}-completion for queries. There
+there is completion for all search keywords such as @code{and},
+@code{from:}, or @code{date:} and also for certain values, i.e., the
+possible values for @code{flag:}, @code{prio:}, @code{mime:}, and
+@code{maildir:}.
+
+Let's look at some examples here.
+
+@itemize
+
+@item Get all messages regarding @emph{bananas}:
+@verbatim
+bananas
+@end verbatim
+
+@item Get all messages regarding @emph{bananas} from @emph{John} with an attachment:
+@verbatim
+from:john and flag:attach and bananas
+@end verbatim
+
+@item Get all messages with subject @emph{wombat} in June 2017
+@verbatim
+subject:wombat and date:20170601..20170630
+@end verbatim
+
+@item Get all messages with PDF attachments in the @t{/projects} folder
+@verbatim
+maildir:/projects and mime:application/pdf
+@end verbatim
+
+@item Get all messages about @emph{Rupert} in the @t{/Sent Items} folder. Note that
+maildirs with spaces must be quoted.
+@verbatim
+"maildir:/Sent Items" and rupert
+@end verbatim
+
+@item Get all important messages which are signed:
+@verbatim
+flag:signed and prio:high
+@end verbatim
+
+@item Get all messages from @emph{Jim} without an attachment:
+@verbatim
+from:jim and not flag:attach
+@end verbatim
+
+@item Get all messages with Alice in one of the contacts-fields (@t{to}, @t{from},
+@t{cc}, @t{bcc}):
+@verbatim
+contact:alice
+@end verbatim
+
+@item Get all unread messages where the subject mentions Ångström: (search is
+case-insensitive and accent-insensitive, so this matches Ångström, angstrom,
+aNGstrøM, ...)
+@verbatim
+subject:Ångström and flag:unread
+@end verbatim
+
+@item Get all unread messages between Mar-2012 and Aug-2013 about some bird:
+@verbatim
+date:20120301..20130831 and nightingale and flag:unread
+@end verbatim
+
+@item Get today's messages:
+@verbatim
+date:today..now
+@end verbatim
+
+@item Get all messages we got in the last two weeks regarding @emph{emacs}:
+@verbatim
+date:2w.. and emacs
+@end verbatim
+
+@item Get messages from the @emph{Mu} mailing list:
+@verbatim
+list:mu-discuss.googlegroups.com
+@end verbatim
+
+Note --- in the @ref{Headers view} you may see the `friendly name' for a
+list; however, when searching you need the real name. You can see the
+real name for a mailing list from the friendly name's tool-tip.
+
+@item Get messages with a subject soccer, Socrates, society, ...; note that
+the `*'-wildcard can only appear as a term's rightmost character:
+@verbatim
+subject:soc*
+@end verbatim
+
+@item Get all messages @emph{not} sent to a mailing-list:
+@verbatim
+NOT flag:list
+@end verbatim
+
+@item Get all mails with attachments with filenames starting with @emph{pic}; note
+that the `*' wildcard can only appear as the term's rightmost character:
+@verbatim
+file:pic*
+@end verbatim
+
+@item Get all messages with PDF-attachments:
+@verbatim
+mime:application/pdf
+@end verbatim
+
+Get all messages with image attachments, and note that the `*' wildcard can
+only appear as the term's rightmost character:
+@verbatim
+mime:image/*
+@end verbatim
+
+Get all messages with files that end in @t{.ppt}; this uses the
+regular-expression support, which is powerful but relatively slow:
+@verbatim
+file:/\.ppt$/
+@end verbatim
+
+@end itemize
+
+@node Bookmarks
+@section Bookmarks
+
+If you have queries that you use often, you may want to store them as
+@emph{bookmarks}. Bookmark searches are available in the main view
+(@pxref{Main view}), header view (@pxref{Headers view}), and message
+view (@pxref{Message view}), using (by default) the key @key{b}
+(@kbd{M-x mu4e-search-bookmark}), or @key{B} (@kbd{M-x
+mu4e-search-bookmark-edit}) which lets you edit the bookmark first.
+
+@subsection Setting up bookmarks
+
+@t{mu4e} provides a number of default bookmarks. Their definition may
+be instructive:
+
+@lisp
+(defcustom mu4e-bookmarks
+ '(( :name "Unread messages"
+ :query "flag:unread AND NOT flag:trashed"
+ :key ?u)
+ ( :name "Today's messages"
+ :query "date:today..now"
+ :key ?t)
+ ( :name "Last 7 days"
+ :query "date:7d..now"
+ :hide-unread t
+ :key ?w)
+ ( :name "Messages with images"
+ :query "mime:image/*"
+ :key ?p))
+ "List of pre-defined queries that are shown on the main screen.
+
+Each of the list elements is a plist with at least:
+:name - the name of the query
+:query - the query expression
+:key - the shortcut key.
+
+Optionally, you add the following:
+:hide - if t, bookmark is hdden from the main-view and speedbar.
+:hide-unread - do not show the counts of unread/total number
+ of matches for the query. This can be useful if a bookmark uses
+ a very slow query. :hide-unread is implied from :hide.
+"
+ :type '(repeat (plist))
+ :group 'mu4e)
+@end lisp
+
+You can replace these or add your own items, by putting in your
+configuration (@file{~/.emacs}) something like:
+@lisp
+(add-to-list 'mu4e-bookmarks
+ '( :name "Big messages"
+ :query "size:5M..500M"
+ :key ?b))
+ @end lisp
+
+This prepends your bookmark to the list, and assigns the key @key{b} to it. If
+you want to @emph{append} your bookmark, you can use @code{t} as the third
+argument to @code{add-to-list}.
+
+In the various @t{mu4e} views, pressing @key{b} lists all the bookmarks
+defined in the echo area, with the shortcut key highlighted. So, to invoke the
+bookmark we just defined (to get the list of "Big Messages"), all you need to
+type is @kbd{bb}.
+
+@subsection Lisp expressions or functions as bookmarks
+
+Instead of using strings, it is also possible to use Lisp expressions as
+bookmarks. Either the expression evaluates to a query string or the expression
+is a function taking no argument that returns a query string.
+
+For example, to get all the messages that are at most a week old in your
+inbox:
+
+@lisp
+(add-to-list 'mu4e-bookmarks
+ '( :name "Inbox messages in the last 7 days"
+ :query (lambda () (concat "maildir:/inbox AND date:"
+ (format-time-string "%Y%m%d"
+ (subtract-time (current-time) (days-to-time 7)))))
+ :key ?w) t)
+@end lisp
+
+Another example where the user is prompted how many days old messages should be
+shown:
+
+@lisp
+(defun my/mu4e-bookmark-num-days-old-query (days-old)
+ (interactive (list (read-number "Show days old messages: " 7)))
+ (let ((start-date (subtract-time (current-time) (days-to-time days-old))))
+ (concat "maildir:/inbox AND date:"
+ (format-time-string "%Y%m%d" start-date))))
+
+(add-to-list 'mu4e-bookmarks
+ `(:name "Inbox messages in the last 7 days"
+ :query ,(lambda () (call-interactively 'my/mu4e-bookmark-num-days-old-query))
+ :key ?o) t)
+@end lisp
+
+It is defining a function to make the code more readable.
+
+@subsection Editing bookmarks before searching
+
+There is also @kbd{M-x mu4e-headers-search-bookmark-edit} (key @key{B}), which
+lets you edit the bookmarked query before invoking it. This can be useful if
+you have many similar queries, but need to change some parameter. For example,
+you could have a bookmark @samp{"date:today..now AND "}@footnote{Not a valid
+search query by itself}, which limits any result to today's messages.
+
+@node Maildir searches
+@section Maildir searches
+
+Maildir searches are quite similar to bookmark searches (see @ref{Bookmarks}),
+with the difference being that the target is always a maildir --- maildir
+queries provide a `traditional' folder-like interface to a search-based e-mail
+client. By default, maildir searches are available in the @ref{Main view},
+@ref{Headers view}, and @ref{Message view}, with the key @key{j}
+(@code{mu4e-jump-to-maildir}). If a prefix argument is given, the maildir
+query can be refined before execution.
+
+@subsection Setting up maildir shortcuts
+
+You can search for maildirs like any other message property
+(e.g. with a query like @t{maildir:/myfolder}), but since it is so common,
+@t{mu4e} offers a shortcut for this.
+
+For this to work, you need to set the variable
+@code{mu4e-maildir-shortcuts} to the list of maildirs you want to have
+quick access to, for example:
+
+@lisp
+(setq mu4e-maildir-shortcuts
+ '( (:maildir "/inbox" :key ?i)
+ (:maildir "/archive" :key ?a)
+ (:maildir "/lists" :key ?l)
+ (:maildir "/work" :key ?w)
+ (:maildir "/sent" :key ?s)))
+@end lisp
+
+This sets @key{i} as a shortcut for the @t{/inbox} folder --- effectively a
+query @t{maildir:/inbox}. There is a special shortcut @key{o} or @key{/} for
+@emph{other} (so don't use those for your own shortcuts!), which allows you to
+choose from @emph{all} maildirs that you have. There is support for
+autocompletion; note that the list of maildirs is determined when @t{mu4e}
+starts; if there are changes in the maildirs while @t{mu4e} is running, you
+need to restart @t{mu4e}.
+
+Each of the folder names is relative to your top-level maildir directory; so
+if you keep your mail in @file{~/Maildir}, @file{/inbox} would refer to
+@file{~/Maildir/inbox}. With these shortcuts, you can jump around your
+maildirs (folders) very quickly --- for example, getting to the @t{/lists}
+folder only requires you to type @kbd{jl}, then change to @t{/work} with
+@kbd{jw}.
+
+While in queries you need to quote folder names (maildirs) with spaces in
+them, you should @emph{not} quote them when used in
+@code{mu4e-maildir-shortcuts}, since @t{mu4e} does that automatically for you.
+
+The very same shortcuts are used by @kbd{M-x mu4e-mark-for-move} (default
+shortcut @key{m}); so, for example, if you want to move a message to the
+@t{/archive} folder, you can do so by typing @kbd{ma}.
+
+@node Other search functionality
+@section Other search functionality
+
+@subsection Navigating through search queries
+You can navigate through previous/next queries using
+@code{mu4e-headers-query-prev} and @code{mu4e-headers-query-next}, which are
+bound to @key{M-left} and @key{M-right}, similar to what some web browsers do.
+
+@t{mu4e} tries to be smart and not record duplicate queries. Also, the number
+of queries remembered has a fixed limit, so @t{mu4e} won't use too much
+memory, even if used for a long time. However, if you want to forget
+previous/next queries, you can use @kbd{M-x mu4e-headers-forget-queries}.
+
+@subsection Narrowing search results
+
+It can be useful to narrow existing search results, that is, to add some
+clauses to the current query to match fewer messages.
+
+For example, suppose you're looking at some mailing list, perhaps by
+jumping to a maildir (@kbd{M-x mu4e-headers-jump-to-maildir}, @key{j}) or
+because you followed some bookmark (@kbd{M-x mu4e-headers-search-bookmark},
+@key{b}). Now, you want to narrow things down to only those messages that have
+attachments.
+
+This is when @kbd{M-x mu4e-headers-search-narrow} (@key{/}) comes in handy. It
+asks for an additional search pattern, which is appended to the current search
+query, in effect getting you the subset of the currently shown headers that
+also match this extra search pattern. @key{\} takes you back to the previous
+query, so, effectively `widens' the search. Technically, narrowing the results
+of query @t{x} with expression @t{y} implies doing a search @t{(x) AND (y)}.
+
+Note that messages that were not in your original search results because
+of @code{mu4e-headers-results-limit} may show up in the narrowed query.
+
+@subsection Including related messages
+@anchor{Including related messages}
+
+It can be useful to not only show the messages that directly match a certain
+query, but also include messages that are related to these messages. That is,
+messages that belong to the same discussion threads are included in the
+results, just like e.g. Gmail does it. You can enable this behavior by setting
+@code{mu4e-headers-include-related} to @code{t}, and you can toggle between
+including/not-including with @key{W}.
+
+Be careful though when e.g. deleting ranges of messages from a certain
+folder --- the list may now also include messages from @emph{other}
+folders.
+
+@subsection Skipping duplicates
+@anchor{Skipping duplicates}
+
+Another useful feature is skipping of @emph{duplicate messages}. When you have
+copies of messages, there's usually little value in including more than one in
+search results. A common reason for having multiple copies of messages is the
+combination of Gmail and @t{offlineimap}, since that is the way the labels /
+virtual folders in Gmail are represented. You can enable skipping duplicates
+by setting @code{mu4e-headers-skip-duplicates} to @code{t}, and you can toggle
+between the skipping/not skipping with @key{V}.
+
+Note, messages are considered duplicates when they have the same
+@t{Message-Id}.
+
+@node Marking
+@chapter Marking
+
+In @t{mu4e}, the common way to do things with messages is a two-step process -
+first you @emph{mark} them for a certain action, then you @emph{execute}
+(@key{x}) those marks. This is similar to the way @t{dired} operates. Marking
+can happen in both the @ref{Headers view} and the @ref{Message view}.
+
+@menu
+* Marking messages::Selecting message do something with them
+* What to mark for::What can we do with them
+* Executing the marks::Do it
+* Trashing messages::Exceptions for mailboxes like Gmail
+* Leaving the headers buffer::Handling marks automatically when leaving
+* Built-in marking functions::Helper functions for dealing with them
+* Custom mark functions::Define your own mark function
+* Adding a new kind of mark::Adding your own marks
+@end menu
+
+@node Marking messages
+@section Marking messages
+
+There are multiple ways to mark messages:
+@itemize
+@item @emph{message at point}: you can put a mark on the message-at-point in
+either the @ref{Headers view} or @ref{Message view}
+@item @emph{region}: you can put a mark on all messages in the current region
+(selection) in the @ref{Headers view}
+@item @emph{pattern}: you can put a mark on all messages in the @ref{Headers
+view} matching a certain pattern with @kbd{M-x mu4e-headers-mark-pattern}
+(@key{%})
+@item @emph{thread/subthread}: You can put a mark on all the messages in the
+thread/subthread at point with @kbd{M-x mu4e-headers-mark-thread} and @kbd{M-x
+mu4e-headers-mark-subthread}, respectively
+@end itemize
+
+@node What to mark for
+@section What to mark for
+
+@t{mu4e} supports a number of marks:
+
+@cartouche
+@verbatim
+mark for/as | keybinding | description
+-------------+-------------+------------------------------
+'something' | *, <insert> | mark now, decide later
+delete | D, <delete> | delete
+flag | + | mark as 'flagged' ('starred')
+move | m | move to some maildir
+read | ! | mark as read
+refile | r | mark for refiling
+trash | d | move to the trash folder
+untrash | = | remove 'trash' flag
+unflag | - | remove 'flagged' mark
+unmark | u | remove mark at point
+unmark all | U | remove all marks
+unread | ? | marks as unread
+action | a | apply some action
+@end verbatim
+@end cartouche
+
+After marking a message, the left-most columns in the headers view indicate
+the kind of mark. This is informative, but if you mark many (say, thousands)
+messages, this slows things down significantly@footnote{this uses an
+Emacs feature called @emph{overlays}, which are slow when used a lot
+in a buffer}. For this reason, you can disable this by setting
+@code{mu4e-headers-show-target} to @code{nil}.
+
+@t{something} is a special kind of mark; you can use it to mark messages
+for `something', and then decide later what the `something' should
+be@footnote{This kind of `deferred marking' is similar to the facility
+in @t{dired}, @t{midnight commander}
+(@url{https://www.midnight-commander.org/}) and the like, and uses the
+same key binding (@key{insert}).} Later, you can set the actual mark
+using @kbd{M-x mu4e-mark-resolve-deferred-marks}
+(@key{#}). Alternatively, @t{mu4e} will ask you when you try to execute
+the marks (@key{x}).
+
+@node Executing the marks
+@section Executing the marks
+
+After you have marked some messages, you can execute them with @key{x}
+(@kbd{M-x mu4e-mark-execute-all}).
+
+A hook, @code{mu4e-mark-execute-pre-hook}, is available which is run
+right before execution of each mark. The hook is called with two
+arguments, the mark and the message itself.
+
+@node Trashing messages
+@section Trashing messages
+
+For regular mailboxes, trashing works like other marks: when executed,
+the message is flagged as trashed. Depending on your mailbox provider,
+the trash flag is used to automatically move the message to the trash
+folder (@code{mu4e-trash-folder}) for instance.
+
+Some mailboxes behave differently however and they don't interpret the
+trash flag. In cases like Gmail, the message must be @emph{moved} to
+the trash folder and the trash flag must not be used.
+
+@node Leaving the headers buffer
+@section Leaving the headers buffer
+
+When you quit or update a headers buffer that has marked messages (for
+example, by doing a new search), @t{mu4e} asks you what to do with them,
+depending on the value of the variable @code{mu4e-headers-leave-behavior} ---
+see its documentation.
+
+@node Built-in marking functions
+@section Built-in marking functions
+
+Some examples of @t{mu4e}'s built-in marking functions.
+
+@itemize
+@item @emph{Mark the message at point for trashing}: press @key{d}
+@item @emph{Mark all messages in the buffer as unread}: press @kbd{C-x h o}
+@item @emph{Delete the messages in the current thread}: press @kbd{T D}
+@item @emph{Mark messages with a subject matching ``hello'' for flagging}:
+press @kbd{% s hello RET}.
+@end itemize
+
+@node Custom mark functions
+@section Custom mark functions
+
+Sometimes, the built-in functions to mark messages may not be sufficient for
+your needs. For this, @t{mu4e} offers an easy way to define your own custom
+mark functions. You can choose one of the custom marker functions by pressing
+@key{&} in the @ref{Headers view} and @ref{Message view}.
+
+Custom mark functions are to be appended to the list
+@code{mu4e-headers-custom-markers}. Each of the elements of this list
+('markers') is a list with two or three elements:
+@enumerate
+@item The name of the marker --- a short string describing this marker. The
+first character of this string determines its shortcut, so these should be
+unique. If necessary, simply prefix the name with a unique character.
+@item a predicate function, taking two arguments @var{msg} and @var{param}.
+@var{msg} is the message plist (see @ref{Message functions}) and @var{param} is
+a parameter provided by the third of the marker elements (see the next
+item). The predicate function should return non-@t{nil} if the message
+matches.
+@item (optionally) a function that is evaluated once, and the result is passed as a
+parameter to the predicate function. This is useful when user-input is needed.
+@end enumerate
+
+Let's look at an example: suppose we want to match all messages that have more
+than @emph{n} recipients --- we could do this with the following recipe:
+
+@lisp
+(add-to-list 'mu4e-headers-custom-markers
+ '("More than n recipients"
+ (lambda (msg n)
+ (> (+ (length (mu4e-message-field msg :to))
+ (length (mu4e-message-field msg :cc))) n))
+ (lambda ()
+ (read-number "Match messages with more recipients than: "))) t)
+@end lisp
+
+After evaluating this expression, you can use it by pressing @key{&} in
+the headers buffer to select a custom marker function, and then @key{M}
+to choose this particular one (@t{M} because it is the first character
+of the description).
+
+As you can see, it's not very hard to define simple functions to match
+messages. There are more examples in the defaults for
+@code{mu4e-headers-custom-markers}; see @file{mu4e-headers.el} and see
+@ref{Extending mu4e} for general information about writing your own functions.
+
+
+@node Adding a new kind of mark
+@section Adding a new kind of mark
+
+It is possible to configure new marks. To do so, one can add entries
+in the list @code{mu4e-marks}. Such an element must have the following form:
+
+@lisp
+(SYMBOL
+ :char STRING
+ :prompt STRING
+ :ask-target (lambda () TARGET)
+ :dyn-target (lambda (TARGET MSG) DYN-TARGET)
+ :show-target (lambda (DYN-TARGET) STRING)
+ :action (lambda (DOCID MSG DYN-TARGET) nil))
+@end lisp
+
+The symbol can be any symbol, except for @code{'unmark} and
+@code{'something}, which are reserved. The rest is a plist with the
+following elements:
+
+@itemize
+@item @code{:char} --- the character to display in the headers view.
+@item @code{:prompt} --- the prompt to use when asking for marks
+(used for example when marking a whole thread).
+@item @code{:ask-target} --- a function run once per bulk-operation, and thus suitable for
+querying the user about a target for move-like marks. If @t{nil}, the
+@t{TARGET} passed to @code{:dyn-target} is @t{nil}.
+@item @code{:dyn-target} --- a function run once per message
+(The message is passed as @t{MSG} to the function). This function allows
+to compute a per-message target, for refile-like marks. If @t{nil}, the
+@t{DYN-TARGET} passed to the @code{:action} is the @t{TARGET} obtained as above.
+@item @code{:show-target} --- how to display the target in the headers view.
+If @code{:show-target} is @t{nil} the @t{DYN-TARGET} is shown (and
+@t{DYN-TARGET} must be a string).
+@item @code{:action} --- the action to apply on the message when the mark is executed.
+@end itemize
+
+As an example, suppose we would like to add a mark for tagging
+messages (GMail-style), then we can run the following code (after
+loading @t{mu4e}):
+
+@lisp
+(add-to-list 'mu4e-marks
+ '(tag
+ :char "g"
+ :prompt "gtag"
+ :ask-target (lambda () (read-string "What tag do you want to add?"))
+ :action (lambda (docid msg target)
+ (mu4e-action-retag-message msg (concat "+" target)))))
+@end lisp
+
+As another example, suppose we would like to ``archive and mark read''
+a message (GMail-style), then we can run the following code (after
+loading @t{mu4e}):
+
+@lisp
+(add-to-list 'mu4e-marks
+ '(archive
+ :char "A"
+ :prompt "Archive"
+ :show-target (lambda (target) "archive")
+ :action (lambda (docid msg target)
+ ;; must come before proc-move since retag runs
+ ;; 'sed' on the file
+ (mu4e-action-retag-message msg "-\\Inbox")
+ (mu4e--server-move docid nil "+S-u-N"))))
+@end lisp
+
+Adding to @code{mu4e-marks} list allows to use the mark in bulk operations
+(for example when tagging a whole thread), but does not bind the mark
+to a key to use at the top-level. This must be done separately. In our
+example:
+
+@lisp
+(mu4e~headers-defun-mark-for tag)
+(mu4e~headers-defun-mark-for archive)
+(define-key mu4e-headers-mode-map (kbd "g") 'mu4e-headers-mark-for-tag)
+(define-key mu4e-headers-mode-map (kbd "A") 'mu4e-headers-mark-for-archive)
+@end lisp
+
+@node Contexts
+@chapter Contexts
+
+@menu
+* What are contexts::Defining the concept
+* Context policies::How to determine the current context
+* Contexts and special folders::Using context variables to determine them
+* Contexts example::How to define contexts
+@end menu
+
+It can be useful to switch between different sets of settings in
+@t{mu4e}; a typical example is the case where you have different e-mail
+accounts for private and work email, each with their own values for
+folders, e-mail addresses, mailservers and so on.
+
+The @code{mu4e-context} system is a @t{mu4e}-specific mechanism to allow
+for that; users can define different @i{contexts} corresponding with
+groups of setting and either manually switch between them, or let
+@t{mu4e} determine the right context based on some user-provided
+function.
+
+Note that there are a number of existing ways to switch accounts in
+@t{mu4e}, for example using the method described in the @ref{Tips and
+Tricks} section of this manual. Those still work --- but the new mechanism
+has the benefit of being a core part of @code{mu4e}, thus allowing for
+deeper integration.
+
+@node What are contexts
+@section What are contexts
+
+Let's see what's contained in a context. Most of it is optional.
+
+A @code{mu4e-context} is Lisp object with the following members:
+@itemize
+@item @t{name}: the name of the context, e.g. @t{work} or @t{private}
+@item @t{vars}:
+an association-list (alist) of variable settings for this account.
+@item @t{enter-func}:
+an (optional) function that takes no parameter and is invoked when entering
+the context. You can use this for extra setup etc.
+@item @t{leave-func}:
+an (optional) function that takes no parameter and is invoked when leaving
+the context. You can use this for clearing things up.
+@item @t{match-func}:
+an (optional) function that takes an @t{MSG} message plist as argument,
+and returns non-@t{nil} if this context matches the situation. @t{mu4e}
+uses the first context that matches, in a couple of situations:
+@itemize
+@item when starting @t{mu4e} to determine the
+starting context; in this case, @t{MSG} is nil. You can use e.g. the
+host you're running or the time of day to determine which context
+matches.
+@item before replying to or forwarding a
+message with the given message plist as parameter, or @t{nil} when
+composing a brand new message. The function should return @t{t} when
+this context is the right one for this message, or @t{nil} otherwise.
+@item when determining the target folders for deleting, refiling etc;
+see @ref{Contexts and special folders}.
+@end itemize
+@end itemize
+
+@t{mu4e} uses a variable @code{mu4e-contexts}, which is a list of such
+objects.
+
+@node Context policies
+@section Context policies
+
+When you have defined contexts and you start @t{mu4e} it decides which
+context to use based on the variable @code{mu4e-context-policy};
+similarly, when you compose a new message, the context is determined
+using @code{mu4e-compose-context-policy}.
+
+For both of these, you can choose one of the following policies:
+@itemize
+@item a symbol @code{always-ask}: unconditionally ask the user what context to pick.
+@end itemize
+
+The other choices @b{only apply if none of the contexts match} (i.e.,
+none of the contexts' match-functions returns @code{t}). We have the
+following options:
+
+@itemize
+@item a symbol @code{ask}: ask the user if @t{mu4e} can't figure
+things out the context by itself (through the match-function). This is a
+good policy if there are no match functions, or if the match functions
+don't cover all cases.
+@item a symbol @code{ask-if-none}: if there's already a context, don't change it;
+otherwise, ask the user.
+@item a symbol @code{pick-first}: pick the first (default) context. This is a
+good choice if
+you want to specify context for special case, and fall back to the first
+one if none match.
+@item @code{nil}: don't change the context; this is useful if you don't change
+contexts very often, and e.g. manually changes contexts with @kbd{M-x
+mu4e-context-switch}.
+@end itemize
+
+@node Contexts and special folders
+@section Contexts and special folders
+
+As we discussed in @ref{Folders} and @ref{Dynamic folders}, @t{mu4e}
+recognizes a number of special folders: @code{mu4e-sent-folder},
+@code{mu4e-drafts-folder}, @code{mu4e-trash-folder} and
+@code{mu4e-refile-folder}.
+
+When you have a headers-buffer with messages that belong to different
+contexts (say, a few different accounts), it is desirable for each of
+them to use the specific folders for their own context --- so, for
+instance, if you trash a message, it needs to go to the trash-folder for
+the account it belongs to, which is not necessarily the current context.
+
+To make this easy to do, whenever @t{mu4e} needs to know the value for
+such a special folder for a given message, it tries to determine the
+appropriate context using @code{mu4e-context-determine} (and policy
+@t{nil}; see @ref{Context policies}). If it finds a matching context, it
+let-binds the @code{vars} for that account, and then determines the
+value for the folder. It does not, however, call the @code{enter-func}
+or @code{leave-func}, since we are not really switching contexts.
+
+In practice, this means that as long as each of the accounts has a good
+@t{match-func}, all message operations automatically find the
+appropriate folders.
+
+@node Contexts example
+@section Example
+
+Let's explain how contexts work by looking at an example. We define two
+contexts, `Private' and `Work' for a fictional user @emph{Alice
+Derleth}.
+
+Note that in this case, we automatically switch to the first context
+when starting; see the discussion in the previous section.
+
+@lisp
+
+ (setq mu4e-contexts
+ `( ,(make-mu4e-context
+ :name "Private"
+ :enter-func (lambda () (mu4e-message "Entering Private context"))
+ :leave-func (lambda () (mu4e-message "Leaving Private context"))
+ ;; we match based on the contact-fields of the message
+ :match-func (lambda (msg)
+ (when msg
+ (mu4e-message-contact-field-matches msg
+ :to "aliced@@home.example.com")))
+ :vars '( ( user-mail-address . "aliced@@home.example.com" )
+ ( user-full-name . "Alice Derleth" )
+ ( message-user-organization . "Homebase" )
+ ( mu4e-compose-signature .
+ (concat
+ "Alice Derleth\n"
+ "Lauttasaari, Finland\n"))))
+ ,(make-mu4e-context
+ :name "Work"
+ :enter-func (lambda () (mu4e-message "Switch to the Work context"))
+ ;; no leave-func
+ ;; we match based on the maildir of the message
+ ;; this matches maildir /Arkham and its sub-directories
+ :match-func (lambda (msg)
+ (when msg
+ (string-match-p "^/Arkham" (mu4e-message-field msg :maildir))))
+ :vars '( ( user-mail-address . "aderleth@@miskatonic.example.com" )
+ ( user-full-name . "Alice Derleth" )
+ ( message-user-organization . "Miskatonic University" )
+ ( mu4e-compose-signature .
+ (concat
+ "Prof. Alice Derleth\n"
+ "Miskatonic University, Dept. of Occult Sciences\n"))))
+
+ ,(make-mu4e-context
+ :name "Cycling"
+ :enter-func (lambda () (mu4e-message "Switch to the Cycling context"))
+ ;; no leave-func
+ ;; we match based on the maildir of the message; assume all
+ ;; cycling-related messages go into the /cycling maildir
+ :match-func (lambda (msg)
+ (when msg
+ (string= (mu4e-message-field msg :maildir) "/cycling")))
+ :vars '( ( user-mail-address . "aderleth@@example.com" )
+ ( user-full-name . "AliceD" )
+ ( mu4e-compose-signature . nil)))))
+
+ ;; set `mu4e-context-policy` and `mu4e-compose-policy` to tweak when mu4e should
+ ;; guess or ask the correct context, e.g.
+
+ ;; start with the first (default) context;
+ ;; default is to ask-if-none (ask when there's no context yet, and none match)
+ ;; (setq mu4e-context-policy 'pick-first)
+
+ ;; compose with the current context is no context matches;
+ ;; default is to ask
+ ;; (setq mu4e-compose-context-policy nil)
+@end lisp
+
+A couple of notes about this example:
+@itemize
+@item You can manually switch the context use @code{M-x mu4e-context-switch},
+by default bound to @kbd{;} in headers, view and main mode.
+The current context appears in the mode-line.
+@item Normally, @code{M-x mu4e-context-switch} does not call the enter or
+leave functions if the 'new' context is the same as the old one.
+However, with a prefix-argument (@kbd{C-u}), you can force @t{mu4e} to
+invoke those function even in that case.
+@item The function @code{mu4e-context-current} returns the current-context;
+the current context is also visible in the mode-line when in
+headers, view or main mode.
+@item You can set any kind of variable; including settings for mail servers etc.
+However, settings such as @code{mu4e-mu-home} are not changeable after
+they have been set without quitting @t{mu4e} first.
+@item @code{leave-func} (if defined) for the context we are leaving, is invoked
+before the @code{enter-func} (if defined) of the
+context we are entering.
+@item @code{enter-func} (if defined) is invoked before setting the variables.
+@item @code{match-func} (if defined) is invoked just before @code{mu4e-compose-pre-hook}.
+@item See the variables @code{mu4e-context-policy} and
+@code{mu4e-compose-context-policy} to tweak what @t{mu4e} should do when
+no context matches (or if you always want to be asked).
+@item Finally, be careful to get the quotations right --- backticks, single quotes
+and commas and note the '.' between variable name and its value.
+@end itemize
+
+@node Dynamic folders
+@chapter Dynamic folders
+
+In @ref{Folders}, we explained how you can set up @t{mu4e}'s special
+folders:
+@lisp
+(setq
+ mu4e-sent-folder "/sent" ;; sent messages
+ mu4e-drafts-folder "/drafts" ;; unfinished messages
+ mu4e-trash-folder "/trash" ;; trashed messages
+ mu4e-refile-folder "/archive") ;; saved messages
+@end lisp
+
+In some cases, having such static folders may not suffice --- perhaps you want
+to change the folders depending on the context. For example, the folder for
+refiling could vary, based on the sender of the message.
+
+To make this possible, instead of setting the standard folders to a string,
+you can set them to be a @emph{function} that takes a message as its
+parameter, and returns the desired folder name. This chapter shows you how to
+do that. For a more general discussion of how to extend @t{mu4e} and writing
+your own functions, see @ref{Extending mu4e}.
+
+If you use @t{mu4e-context}, see @ref{Contexts and special folders} for
+what that means for these special folders.
+
+@menu
+* Smart refiling:: Automatically choose the target folder
+* Other dynamic folders:: Flexible folders for sent, trash, drafts
+@end menu
+
+
+@node Smart refiling
+@section Smart refiling
+
+When refiling messages, perhaps to archive them, it can be useful to have
+different target folders for different messages, based on some property of
+those message --- smart refiling.
+
+To accomplish this, we can set the refiling folder (@code{mu4e-refile-folder})
+to a function that returns the actual refiling folder for the particular
+message. An example should clarify this:
+
+@lisp
+(setq mu4e-refile-folder
+ (lambda (msg)
+ (cond
+ ;; messages to the mu mailing list go to the /mu folder
+ ((mu4e-message-contact-field-matches msg :to
+ "mu-discuss@@googlegroups.com")
+ "/mu")
+ ;; messages sent directly to some spefic address me go to /private
+ ((mu4e-message-contact-field-matches msg :to "me@@example.com")
+ "/private")
+ ;; messages with football or soccer in the subject go to /football
+ ((string-match "football\\|soccer"
+ (mu4e-message-field msg :subject))
+ "/football")
+ ;; messages sent by me go to the sent folder
+ ((mu4e-message-sent-by-me msg
+ (mu4e-personal-addresses))
+ mu4e-sent-folder)
+ ;; everything else goes to /archive
+ ;; important to have a catch-all at the end!
+ (t "/archive"))))
+@end lisp
+
+@noindent
+This can be very powerful; you can select some messages in the headers view,
+then press @key{r}, and have them all marked for refiling to their particular
+folders.
+
+Some notes:
+@itemize
+@item We set @code{mu4e-refile-folder} to an anonymous (@t{lambda}) function. This
+function takes one argument, a message plist@footnote{a property list
+describing a message}. The plist corresponds to the message at point. See
+@ref{Message functions} for a discussion on how to deal with them.
+@item In our function, we use a @t{cond} control structure; the function
+returns the first of the clauses that matches. It's important to make the last
+clause a catch-all, so we always return @emph{some} folder.
+@item We use
+the convenience function @code{mu4e-message-contact-field-matches},
+which evaluates to @code{t} if any of the names or e-mail addresses in a
+contact field (in this case, the @t{To:}-field) matches the regular
+expression. With @t{mu4e} version 0.9.16 or newer, the contact field can
+in fact be a list instead of a single value, such as @code{'(:to :cc)'}.
+@end itemize
+
+@node Other dynamic folders
+@section Other dynamic folders
+
+Using the same mechanism, you can create dynamic sent-, trash-, and
+drafts-folders. The message-parameter you receive for the sent and drafts
+folder is the @emph{original} message, that is, the message you reply to, or
+forward, or edit. If there is no such message (for example when composing a
+brand new message) the message parameter is @t{nil}.
+
+Let's look at an example. Suppose you want a different trash folder for
+work-email. You can achieve this with something like:
+
+@lisp
+(setq mu4e-trash-folder
+(lambda (msg)
+;; the 'and msg' is to handle the case where msg is nil
+(if (and msg
+(mu4e-message-contact-field-matches msg :to "me@@work.example.com"))
+"/trash-work"
+"/trash")))
+@end lisp
+
+@noindent
+Good to remember:
+@itemize
+@item The @var{msg} parameter you receive in the function refers to the
+@emph{original message}, that is, the message being replied to or
+forwarded. When re-editing a message, it refers to the message being
+edited. When you compose a totally new message, the @var{msg} parameter is
+@code{nil}.
+@item When re-editing messages, the value of @code{mu4e-drafts-folder} is ignored.
+@end itemize
+
+
+@node Actions
+@chapter Actions
+
+@t{mu4e} lets you define custom actions for messages in @ref{Headers view}
+and for both messages and attachments in @ref{Message view}. Custom
+actions allow you to easily extend @t{mu4e} for specific needs --- for example,
+marking messages as spam in a spam filter or applying an attachment with a
+source code patch.
+
+You can invoke the actions with key @key{a} for actions on messages, and key
+@key{A} for actions on attachments.
+
+For general information extending @t{mu4e} and writing your own functions, see
+@ref{Extending mu4e}.
+
+@menu
+* Defining actions::How to create an action
+* Headers view actions::Doing things with message headers
+* Message view actions::Doing things with messages
+* MIME-part actions::Doing things with MIME-parts such as attachments
+* Example actions::Some more examples
+@end menu
+
+@node Defining actions
+@section Defining actions
+
+Defining a new custom action comes down to writing an elisp-function to do the
+work. Functions that operate on messages receive a @var{msg} parameter, which
+corresponds to the message at point. Something like:
+@lisp
+(defun my-action-func (msg)
+ "Describe my message function."
+ ;; do stuff
+ )
+@end lisp
+
+@noindent
+Functions that operate on attachments receive a @var{msg} parameter, which
+corresponds to the message at point, and an @var{attachment-num}, which is the
+number of the attachment as seen in the message view. An attachment function
+looks like:
+@lisp
+(defun my-attachment-action-func (msg attachment-num)
+ "Describe my attachment function."
+ ;; do stuff
+ )
+@end lisp
+
+@noindent
+After you have defined your function, you can add it to the list of
+actions@footnote{Instead of defining the functions separately, you can
+obviously also add a @code{lambda}-function directly to the list; however,
+separate functions are easier to change}, either @code{mu4e-headers-actions},
+@code{mu4e-view-actions} or @code{mu4e-view-mime-part-actions}. The
+format@footnote{Note, the format of the actions has changed since version
+0.9.8.4, and you must change your configuration to use the new format;
+@t{mu4e} warns you when you are using the old format.} of each action is a
+cons-cell, @code{(DESCRIPTION . VALUE)}; see below for some examples. If your
+shortcut is not also the first character of the description, simply prefix the
+description with that character.
+
+Let's look at some examples.
+
+@node Headers view actions
+@section Headers view actions
+
+Suppose we want to inspect the number of recipients for a message in the
+@ref{Headers view}. We add the following to our configuration:
+
+@lisp
+(defun show-number-of-recipients (msg)
+ "Display the number of recipients for the message at point."
+ (message "Number of recipients: %d"
+ (+ (length (mu4e-message-field msg :to))
+ (length (mu4e-message-field msg :cc)))))
+
+;; define 'N' (the first letter of the description) as the shortcut
+;; the 't' argument to add-to-list puts it at the end of the list
+(add-to-list 'mu4e-headers-actions
+ '("Number of recipients" . show-number-of-recipients) t)
+@end lisp
+
+After evaluating this, @kbd{a N} in the headers view shows the number of
+recipients for the message at point.
+
+@node Message view actions
+@section Message view actions
+
+As another example, suppose we would like to search for messages by the
+sender of the message at point:
+
+@lisp
+(defun search-for-sender (msg)
+ "Search for messages sent by the sender of the message at point."
+ (mu4e-headers-search
+ (concat "from:"
+ (mu4e-contact-email (car (mu4e-message-field msg :from))))))
+
+;; define 'x' as the shortcut
+(add-to-list 'mu4e-view-actions
+ '("xsearch for sender" . search-for-sender) t)
+@end lisp
+
+@indent
+If you wonder why we use @code{car}, remember that the @t{From:}-field
+is a list of @code{(:name NAME :email EMAIL)} plists; so this code gets
+us the e-mail address of the first in the list. @t{From:}-fields rarely
+have more that one address.
+
+@node MIME-part actions
+@section MIME-part actions
+
+Finally, let's define a MIME-part action. As mentioned, MIME-part
+functions receive @emph{2} arguments, the message and the attachment
+number to use.
+
+The following example action counts the number of lines in an
+attachment, and defines @key{n} as its shortcut key (the @key{n} is
+prefixed to the description).
+
+@lisp
+(defun count-lines-in-attachment (msg attachnum)
+ "Count the number of lines in an attachment."
+ (mu4e-view-pipe-attachment msg attachnum "wc -l"))
+
+;; defining 'n' as the shortcut
+(add-to-list 'mu4e-view-mime-part-actions
+ '("ncount lines" . count-lines-in-attachment) t)
+@end lisp
+
+@node Example actions
+@section Example actions
+
+@t{mu4e} includes a number of example actions in the file
+@file{mu4e-actions.el} in the source distribution (see @kbd{C-h f
+mu4e-action-TAB}). For example, for viewing messages in an external web
+browser.
+
+@node Extending mu4e
+@chapter Extending mu4e
+
+@t{mu4e} is designed to be easily extensible --- that is, write your own
+emacs-lisp to make @t{mu4e} behave exactly as you want. Here, we provide some
+guidelines for doing so.
+
+@menu
+* Extension points::Where to hook into @t{mu4e}
+* Available functions::General helper functions
+* Message functions::Working with messages
+* Contact functions::Working with contacts
+* Utility functions::Miscellaneous helpers
+@end menu
+
+@node Extension points
+@section Extension points
+
+There are a number of places where @t{mu4e} lets you plug in your own
+functions:
+@itemize
+@item Custom functions for message header --- see @ref{HV Custom headers}
+@item Using message-specific folders for drafts, trash, sent messages and
+refiling, based on a function --- see @ref{Dynamic folders}
+@item Using an attachment-specific download-directory --- see the
+variable @code{mu4e-attachment-dir}.
+@item Apply a function to a message in the headers view -
+see @ref{Headers view actions}
+@item Apply a function to a message in the message view ---
+see @ref{Message view actions}
+@item Add a new kind of mark for use in the headers view
+- see @ref{Adding a new kind of mark}
+@item Apply a function to a MIME-part --- see @ref{MIME-part actions}
+@item Custom function to mark certain messages ---
+see @ref{Custom mark functions}
+@item Using various @emph{mode}-hooks, @code{mu4e-compose-pre-hook} (see
+@ref{Compose hooks}), @code{mu4e-index-updated-hook} (see @ref{FAQ})
+@end itemize
+
+@noindent
+You can also write your own functions without using the above. If you
+want to do so, key useful functions are @code{mu4e-message-at-point}
+(see below), @code{mu4e-headers-for-each} (to iterate over all
+headers, see its docstring) and @code{mu4e-view-for-each-part} (to
+iterate over all parts/attachments, see its docstring). There is also
+@code{mu4e-view-for-each-uri} to iterate of all the URIs in the
+current message.
+
+Another useful function is
+@code{mu4e-headers-find-if} which searches for a message matching a
+certain pattern; again, see its docstring.
+
+@node Available functions
+@section Available functions
+
+The whole of @t{mu4e} consists of hundreds of elisp functions. However,
+the majority of those are for @emph{internal} use only; you can
+recognize them easily, because they all start with @code{mu4e~} or
+@code{mu4e--}. These functions make all kinds of assumptions, and they
+are subject to change, and should therefore @emph{not} be used. The same
+is true for @emph{variables} with the same prefix; don't touch them. Let
+me repeat that:
+@verbatim
+Do not use mu4e~... or mu4e-- functions or variables!
+@end verbatim
+
+@noindent
+In addition, you should use functions in the right context; functions
+that start with @t{mu4e-view-} are only applicable to the message view,
+while functions starting with @t{mu4e-headers-} are only applicable to
+the headers view. Functions without such prefixes are applicable
+everywhere.
+
+@node Message functions
+@section Message functions
+
+Many functions in @t{mu4e} deal with message plists (property
+lists). They contain information about messages, such as sender and
+recipient, subject, date and so on. To deal with these plists, there are
+a number of @code{mu4e-message-} functions (in @file{mu4e-message.el}),
+such as @code{mu4e-message-field} and @code{mu4e-message-at-point}, and
+a shortcut to combine the two, @code{mu4e-message-field-at-point}.
+
+For example, to get the subject of the message at point, in either the headers
+view or the message view, you could write:
+@lisp
+(mu4e-message-field (mu4e-message-at-point) :subject)
+@end lisp
+@noindent
+Note that:
+@itemize
+@item The contact fields (To, From, Cc, Bcc) are lists of cons-pairs
+@code{(name . email)}; @code{name} may be @code{nil}. So, for example:
+@lisp
+ (mu4e-message-field some-msg :to)
+ ;; => (("Jack" . "jack@@example.com") (nil . "foo@@example.com"))
+@end lisp
+
+If you are only looking for a match in this list (e.g., ``Is Jack one of the
+recipients of the message?''), there is a convenience function
+@code{mu4e-message-contact-field-matches} to make this easy.
+@item The message body is only available in the message view, not in the
+headers view.
+@end itemize
+
+Note that in headers-mode, you only have access to a reduced message
+plist, without the information about the message-body or mime-parts;
+@t{mu4e} does this for performance reasons. And even in view-mode, you
+do not have access to arbitrary message-headers.
+
+However, it is possible to get the information indirectly, using the
+raw-message and some third-party tool like @t{procmail}'s @t{formail}:
+
+@lisp
+(defun my-mu4e-any-message-field-at-point (hdr)
+ "Quick & dirty way to get an arbitrary header HDR at
+point. Requires the 'formail' tool from procmail."
+ (replace-regexp-in-string "\n$" ""
+ (shell-command-to-string
+ (concat "formail -x " hdr " -c < "
+ (shell-quote-argument (mu4e-message-field-at-point :path))))))
+@end lisp
+
+@node Contact functions
+@section Contact functions
+
+It can sometimes be useful to discard or rewrite the contact information
+that @t{mu4e} provides, for example to fix spelling errors, or omit
+unwanted contacts.
+
+To handle this, @t{mu4e} provides @code{mu4e-contact-process-function},
+which, if defined, is applied to each contact. If the result is @t{nil},
+the contact is discarded, otherwise the (modified or not) contact
+information is used.
+
+Each contact is a full e-mail address as you would see in a
+contact-field of an e-mail message, e.g.,
+@verbatim
+"Foo Bar" <foo.bar@example.com>
+@end verbatim
+or
+@verbatim
+cuux@example.com
+@end verbatim
+
+An example @code{mu4e-contact-process-function} might look like:
+
+@lisp
+(defun my-contact-processor (contact)
+ (cond
+ ;; remove unwanted
+ ((string-match-p "evilspammer@@example.com" contact) nil)
+ ((string-match-p "noreply" contact) nil)
+ ;;
+ ;; jonh smiht --> John Smith
+ ((string-match "jonh smiht" contact)
+ (replace-regexp-in-string "jonh smiht" "John Smith" contact))
+ (t contact)))
+
+(setq mu4e-contact-process-function 'my-contact-processor)
+@end lisp
+
+
+@node Utility functions
+@section Utility functions
+
+@file{mu4e-utils} contains a number of utility functions; we list a few
+here. See their docstrings for details:
+@itemize
+@item @code{mu4e-read-option}: read one option from a list. For example:
+@lisp
+(mu4e-read-option "Choose an animal: "
+'(("Monkey" . monkey) ("Gnu" . gnu) ("xMoose" . moose)))
+@end lisp
+The user is presented with:
+@example
+Choose an animal: [M]onkey, [G]nu, [x]Moose
+@end example
+@item @code{mu4e-ask-maildir}: ask for a maildir; try one of the
+shortcuts (@code{mu4e-maildir-shortcuts}), or the full set of available
+maildirs.
+@item @code{mu4e-running-p}: return @code{t} if the @t{mu4e} process is
+running, @code{nil} otherwise.
+@item @code{(mu4e-user-mail-address-p addr)}: return @code{t} if @var{addr} is
+one of the user's e-mail addresses (as per @code{(mu4e-personal-addresses)}).
+@item @code{mu4e-log} logs to the @t{mu4e} debugging log if it is enabled;
+see @code{mu4e-toggle-logging}.
+@item @code{mu4e-message}, @code{mu4e-warning}, @code{mu4e-error} are the
+@t{mu4e} equivalents of the normal elisp @code{message},
+@code{user-error} and @code{error} functions.
+@end itemize
+
+@node Other tools
+@appendix Other tools
+
+In this chapter, we discuss some ways in which @t{mu4e} can cooperate
+with other tools.
+
+@menu
+* Emacs default::Making mu4e the default emacs e-mail program
+* Emacs bookmarks::Using Emacs' bookmark system
+* Org-mode links::Adding mu4e to your organized life
+* Org-contacts::Hooking up with org-contacts
+* BBDB::Hooking up with the Insidious Big Brother Database
+* iCalendar::Enabling iCalendar invite processing
+* Sauron::Getting new mail notifications with Sauron
+* Speedbar::A special frame with your folders
+* Dired:: Attaching files using @t{dired}
+* Hydra:: Custom shortcut menus
+@end menu
+
+@node Emacs default
+@section Emacs default
+
+Emacs allows you to select an e-mail program as the default
+program it uses when you press @key{C-x m} (@code{compose-mail}), call
+@code{report-emacs-bug} and so on. If you want to use @t{mu4e} for this,
+you can do so by adding the following to your configuration:
+
+@lisp
+(setq mail-user-agent 'mu4e-user-agent)
+@end lisp
+
+Similarly, to specify @t{mu4e} as your preferred method for reading
+mail, customize the variable @code{read-mail-command}.
+
+@lisp
+(set-variable 'read-mail-command 'mu4e)
+@end lisp
+
+(@pxref{Top,,Emacs,Sending Mail, Mail Methods})
+
+
+@node Emacs bookmarks
+@section Emacs bookmarks
+
+@t{mu4e} supports linking to the message-at-point through the normal
+Emacs built-in bookmark system. The links are based on the message's
+message-id, and thus the bookmarks stay valid even if you move the
+message around.
+
+
+@node Org-mode links
+@section Org-mode links
+
+It can be useful to include links to e-mail messages or search queries
+in your org-mode files. @t{mu4e} supports this by default, unless you
+set @t{mu4e-support-org} to @code{nil}.
+
+You can use the normal @t{org-mode} mechanisms to store links:
+@kbd{M-x org-store-link} stores a link to a particular message when
+you are in @ref{Message view}. When you are in @ref{Headers view},
+@kbd{M-x org-store-link} links to the @emph{query} if
+@code{mu4e-org-link-query-in-headers-mode} is non-@code{nil}, and to
+the particular message otherwise (which is the default). You can
+customize the link description using @code{mu4e-org-link-desc-func}.
+
+You can insert this link later with @kbd{M-x org-insert-link}. From
+@t{org-mode}, you can go to the query or message the link points to
+with either @kbd{M-x org-agenda-open-link} in agenda buffers, or
+@kbd{M-x org-open-at-point} elsewhere --- both typically bound to
+@kbd{C-c C-o}.
+
+You can also directly @emph{capture} such links --- for example, to
+add e-mail messages to your todo-list. For that, @t{mu4e-org} has a
+function @code{mu4e-org-store-and-capture}. This captures the
+message-at-point (or header --- see the discussion on
+@code{mu4e-org-link-query-in-headers-mode} above), then calls
+@t{org-mode}'s capture functionality.
+
+You can add some specific capture-template for this. In your capture
+templates, the following mu4e-specific values are available:
+
+@cartouche
+@verbatim
+item | description
+-----------------------------------------------------+------------------------
+%:date, %:date-timestamp, %d:date-timestamp-inactive | date, org timestamps
+%:from, %:fromname, %:fromaddress | sender, name/address
+%:to, %:toname, %:toaddress | recipient, name/address
+%:maildir | maildir for the message
+%:message-id | message-id
+%:path | file system path
+%:subject | message subject
+@end verbatim
+@end cartouche
+
+For example, to add a message to your todo-list, and set a deadline
+for processing it within two days, you could add this to
+@code{org-capture-templates}:
+
+@lisp
+ ("P" "process-soon" entry (file+headline "todo.org" "Todo")
+ "* TODO %:fromname: %a %?\nDEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))")
+@end lisp
+
+If you use the functionality a lot, you may want to define
+key-bindings for that in headers and view mode:
+
+@lisp
+ (define-key mu4e-headers-mode-map (kbd "C-c c") 'mu4e-org-store-and-capture)
+ (define-key mu4e-view-mode-map (kbd "C-c c") 'mu4e-org-store-and-capture)
+@end lisp
+
+
+@node Org-contacts
+@section Org-contacts
+
+Note, @t{mu4e} supports built-in address autocompletion; @ref{Address
+autocompletion}, and that is the recommended way to do this. However, it
+is also possible to manage your addresses with @t{org-mode}, using
+@t{org-contacts}@footnote{@url{https://julien.danjou.info/projects/emacs-packages#org-contacts}}.
+
+@t{mu4e-actions} defines a useful action (@ref{Actions}) for adding a
+contact based on the @t{From:}-address in the message at point. To
+enable this, add to your configuration something like:
+
+@lisp
+ (setq mu4e-org-contacts-file <full-path-to-your-org-contacts-file>)
+ (add-to-list 'mu4e-headers-actions
+ '("org-contact-add" . mu4e-action-add-org-contact) t)
+ (add-to-list 'mu4e-view-actions
+ '("org-contact-add" . mu4e-action-add-org-contact) t)
+@end lisp
+
+@noindent
+After this, you should be able to add contacts using @key{a o} in the
+headers view and the message view, using the @t{org-capture} mechanism.
+Note, the shortcut character @key{o} is due to the first character of
+@t{org-contact-add}.
+
+@node BBDB
+@section BBDB
+
+Note, @t{mu4e} supports built-in address autocompletion; @ref{Address
+autocompletion}, and that is the recommended way to do this. However, it
+is also possible to manage your addresses with the current (2015-06-23)
+development release of @t{BBDB}, or releases of @t{BBDB} after
+3.1.2.@footnote{@url{https://savannah.nongnu.org/projects/bbdb/}}.
+
+To enable BBDB, add to your @file{~/.emacs} (or its moral equivalent,
+such as @file{~/.emacs.d/init.el}) the following @emph{after} the
+@code{(require 'mu4e)} line:
+
+@lisp
+ ;; Load BBDB (Method 1)
+ (require 'bbdb-loaddefs)
+ ;; OR (Method 2)
+ ;; (require 'bbdb-loaddefs "/path/to/bbdb/lisp/bbdb-loaddefs.el")
+ ;; OR (Method 3)
+ ;; (autoload 'bbdb-insinuate-mu4e "bbdb-mu4e")
+ ;; (bbdb-initialize 'message 'mu4e)
+
+ (setq bbdb-mail-user-agent 'mu4e-user-agent)
+ (setq mu4e-view-mode-hook 'bbdb-mua-auto-update)
+ (setq mu4e-compose-complete-addresses nil)
+ (setq bbdb-mua-pop-up t)
+ (setq bbdb-mua-pop-up-window-size 5)
+ (setq mu4e-view-show-addresses t)
+@end lisp
+
+@noindent
+After this, you should be able to:
+@itemize
+@item In mu4e-view mode, add the sender of the email to BBDB with @key{C-u :}
+@item Tab-complete addresses from BBDB when composing emails
+@item View the BBDB contact while viewing a message
+@end itemize
+
+@node iCalendar
+@section iCalendar
+
+When Gnus' article-mode is chosen (@ref{Message view}), it is possible
+to view and reply to iCalendar events. To enable this feature, add
+
+@lisp
+(require 'mu4e-icalendar)
+(mu4e-icalendar-setup)
+@end lisp
+
+to your configuration. If you want that the original invitation message
+be automatically trashed after sending the message created by clicking
+on the buttons “Accept”, “Tentative”, or “Decline”, also add:
+
+@lisp
+(setq mu4e-icalendar-trash-after-reply t)
+@end lisp
+
+When you reply to an iCal event, a line may be automatically added to
+the diary file of your choice. You can specify that file with
+
+@lisp
+(setq mu4e-icalendar-diary-file "/path/to/your/diary")
+@end lisp
+
+Note that, if the specified file is not your main diary file, add
+@t{#include "/path/to/your/diary"} to you main diary file to display
+the events.
+
+To enable optional iCalendar→Org sync functionality, add the following:
+
+@lisp
+(setq gnus-icalendar-org-capture-file "~/org/notes.org")
+(setq gnus-icalendar-org-capture-headline '("Calendar"))
+(gnus-icalendar-org-setup)
+@end lisp
+
+Both the capture file and the headline(s) inside it must already exist.
+
+By default, @code{gnus-icalendar-org-setup} adds a temporary capture
+template to the variable @code{org-capture-templates}, with the
+description ``used by gnus-icalendar-org'', and the shortcut key ``#''.
+If you want to use your own template, create it using the same key and
+description. This will prevent the temporary one from being installed
+next time you @code{gnus-icalendar-org-setup} is called.
+
+The full default capture template is:
+
+@lisp
+("#" "used by gnus-icalendar-org" entry
+ (file+olp ,gnus-icalendar-org-capture-file
+ ,gnus-icalendar-org-capture-headline)
+ "%i" :immediate-finish t)
+@end lisp
+
+where the values of the variables @code{gnus-icalendar-org-capture-file}
+and @code{gnus-icalendar-org-capture-headline} are inserted via macro
+expansion.
+
+If, for example, you wanted to store ical events in a date tree,
+prompting for the date, you could use the following:
+
+@lisp
+("#" "used by gnus-icalendar-org" entry
+ (file+olp+datetree path-to-capture-file)
+ "%i" :immediate-finish t :time-prompt t)
+@end lisp
+
+Note that the default behaviour for @code{datetree} targets in this
+situation is to store the event at the date that you capture it, not at
+the date that it is scheduled. That's why I've suggested using the
+@code{:timeprompt t} argument. This gives you an opportunity to set the
+time to the correct value yourself.
+
+You can extract the event time directly, and have the @code{org-capture}
+functions use that to set the @code{datetree} location:
+
+@lisp
+(defun my-catch-event-time (orig-fun &rest args)
+ "Set org-overriding-default-time to the start time of the capture event"
+ (let ((org-overriding-default-time (date-to-time
+ (gnus-icalendar-event:start (car args)))))
+ (apply orig-fun args)))
+
+(advice-add 'gnus-icalendar:org-event-save :around #'my-catch-event-time)
+@end lisp
+
+If you do this, you'll want to omit the @code{:timeprompt t} setting
+from your capture template.
+
+@node Sauron
+@section Sauron
+
+The Emacs package @t{sauron}@footnote{Sauron can be found at
+@url{https://github.com/djcb/sauron}, or in the MELPA (Milkypostman’s
+Emacs Lisp Package Archive) package repository at
+@url{https://melpa.org/}} (by the same author) can be used to
+get notifications about new mails. If you run something like the below
+script from your @t{crontab} (or have some other way of having it
+execute every @emph{n} minutes), you receive notifications in the
+@t{sauron}-buffer when new messages arrive.
+
+@verbatim
+#!/bin/sh
+
+# the mu binary
+MU=mu
+
+# put the path to your Inbox folder here
+CHECKDIR="/home/$LOGNAME/Maildir/Inbox"
+
+sauron_msg () {
+DBUS_COOKIE="/home/$LOGNAME/.sauron-dbus"
+if test "x$DBUS_SESSION_BUS_ADDRESS" = "x"; then
+ if test -e $DBUS_COOKIE; then
+ export DBUS_SESSION_BUS_ADDRESS="`cat $DBUS_COOKIE`"
+ fi
+fi
+if test -n "x$DBUS_SESSION_BUS_ADDRESS"; then
+ dbus-send --session \
+ --dest="org.gnu.Emacs" \
+ --type=method_call \
+ "/org/gnu/Emacs/Sauron" \
+ "org.gnu.Emacs.Sauron.AddMsgEvent" \
+ string:shell uint32:3 string:"$1"
+fi
+}
+
+#
+# -mmin -5: consider only messages that were created / changed in the
+# the last 5 minutes
+#
+for f in `find $CHECKDIR -mmin -5 -a -type f -not -iname '.uidvalidity'`; do
+ subject=`$MU view $f | grep '^Subject:' | sed 's/^Subject://'`
+ sauron_msg "mail: $subject"
+done
+@end verbatim
+
+@noindent
+You might want to put:
+@lisp
+(setq sauron-dbus-cookie t)
+@end lisp
+@noindent
+in your setup, to allow the script to find the D-Bus session bus, even when
+running outside its session.
+
+@node Speedbar
+@section Speedbar
+
+@code{speedbar} is an Emacs-extension that shows navigational
+information for an Emacs buffer in a separate frame. Using
+@code{mu4e-speedbar}, @t{mu4e} lists your bookmarks and maildir
+folders and allows for one-click access to them.
+
+To enable this, add @t{(require 'mu4e-speedbar)} to your configuration;
+then, all you need to do to activate it is @kbd{M-x speedbar}. Then,
+when then switching to the @ref{Main view}, the speedbar-frame is
+updated with your bookmarks and maildirs.
+
+For speed reasons, the list of maildirs is determined when @t{mu4e}
+starts; if the list of maildirs changes while @t{mu4e} is running, you
+need to restart @t{mu4e} to have those changes reflected in the speedbar
+and in other places that use this list, such as auto-completion when
+jumping to a maildir.
+
+@node Dired
+@section Dired
+
+It is possible to attach files to @t{mu4e} messages using @t{dired}
+(@ref{Dired,,emacs}), using the following steps (based on a post on
+the @t{mu-discuss} mailing list by @emph{Stephen Eglen}).
+
+
+@lisp
+(add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode)
+@end lisp
+
+Then, mark the file(s) in @t{dired} you would like to attach and press
+@t{C-c RET C-a}, and you'll be asked whether to attach them to an
+existing message, or create a new one.
+
+@node Hydra
+@section Hydra
+
+People sometimes ask about having multi-character shortcuts for
+bookmarks; an easy way to achieve this, is by using an emacs package
+called Hydra@footnote{@url{https://github.com/abo-abo/hydra}}.
+
+With Hydra installed, we can add multi-character shortcuts, for instance:
+@lisp
+(defhydra my-mu4e-bookmarks-work (:color blue)
+ "work bookmarks"
+ ("b" (mu4e-headers-search "banana AND maildir:/work") "banana")
+ ("u" (mu4e-headers-search "flag:unread AND maildir:/work") "unread"))
+
+(defhydra my-mu4e-bookmarks-personal (:color blue)
+ "personal bookmarks"
+ ("c" (mu4e-headers-search "capybara AND maildir:/personal") "capybara")
+ ("u" (mu4e-headers-search "flag:unread AND maildir:/personal") "unread"))
+
+(defhydra my-mu4e-bookmarks (:color blue)
+ "mu4e bookmarks"
+ ("p" (my-mu4e-bookmarks-personal/body) "Personal")
+ ("w" (my-mu4e-bookmarks-work/body) "Work"))
+
+Now, you can bind a convenient key to my-mu4e-bookmarks/body.
+@end lisp
+
+@node Example configs
+@appendix Example configs
+
+In this chapter, we show some example configurations. While it is very useful
+to see some working settings, we'd like to warn against blindly copying such
+things.
+
+@menu
+* Minimal configuration::Simplest configuration to get you going
+* Longer configuration::A more extensive setup
+* Gmail configuration::GMail-specific setup
+* Other settings:CONF Other settings. Some other useful configuration
+
+@end menu
+
+@node Minimal configuration
+@section Minimal configuration
+
+An (almost) minimal configuration for @t{mu4e} might look like this --- as you
+see, most of it is commented-out.
+
+@lisp
+;; example configuration for mu4e
+
+;; make sure mu4e is in your load-path
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+;; these must start with a "/", and must exist
+;; (i.e.. /home/user/Maildir/sent must exist)
+;; you use e.g. 'mu mkdir' to make the Maildirs if they don't
+;; already exist
+
+;; below are the defaults; if they do not exist yet, mu4e offers to
+;; create them. they can also functions; see their docstrings.
+;; (setq mu4e-sent-folder "/sent")
+;; (setq mu4e-drafts-folder "/drafts")
+;; (setq mu4e-trash-folder "/trash")
+
+;; smtp mail setting; these are the same that `gnus' uses.
+(setq
+ message-send-mail-function 'smtpmail-send-it
+ smtpmail-default-smtp-server "smtp.example.com"
+ smtpmail-smtp-server "smtp.example.com"
+ smtpmail-local-domain "example.com")
+@end lisp
+
+
+@node Longer configuration
+@section Longer configuration
+
+A somewhat longer configuration, showing some more things that you can
+customize.
+
+@lisp
+;; example configuration for mu4e
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+;; the next are relative to the root maildir
+;; (see `mu info`).
+;; instead of strings, they can be functions too, see
+;; their docstring or the chapter 'Dynamic folders'
+(setq mu4e-sent-folder "/sent"
+ mu4e-drafts-folder "/drafts"
+ mu4e-trash-folder "/trash")
+
+;; the maildirs you use frequently; access them with 'j' ('jump')
+(setq mu4e-maildir-shortcuts
+ '((:maildir "/archive" :key ?a)
+ (:maildir "/inbox" :key ?i)
+ (:maildir "/work" :key ?w)
+ (:maildir "/sent" :key ?s)))
+
+;; the headers to show in the headers list -- a pair of a field
+;; and its width, with `nil' meaning 'unlimited'
+;; (better only use that for the last field.
+;; These are the defaults:
+(setq mu4e-headers-fields
+ '( (:date . 25) ;; alternatively, use :human-date
+ (:flags . 6)
+ (:from . 22)
+ (:subject . nil))) ;; alternatively, use :thread-subject
+
+;; program to get mail; alternatives are 'fetchmail', 'getmail'
+;; isync or your own shellscript. called when 'U' is pressed in
+;; main view.
+
+;; If you get your mail without an explicit command,
+;; use "true" for the command (this is the default)
+(setq mu4e-get-mail-command "offlineimap")
+
+;; general emacs mail settings; used when composing e-mail
+;; the non-mu4e-* stuff is inherited from emacs/message-mode
+(setq mu4e-compose-reply-to-address "foo@@bar.example.com"
+ user-mail-address "foo@@bar.example.com"
+ user-full-name "Foo X. Bar")
+(setq mu4e-compose-signature
+ "Foo X. Bar\nhttp://www.example.com\n")
+
+;; smtp mail setting
+(setq
+ message-send-mail-function 'smtpmail-send-it
+ smtpmail-default-smtp-server "smtp.example.com"
+ smtpmail-smtp-server "smtp.example.com"
+ smtpmail-local-domain "example.com"
+
+ ;; if you need offline mode, set these -- and create the queue dir
+ ;; with 'mu mkdir', i.e.. mu mkdir /home/user/Maildir/queue
+ smtpmail-queue-mail nil
+ smtpmail-queue-dir "/home/user/Maildir/queue/cur")
+
+;; don't keep message buffers around
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+
+@node Gmail configuration
+@section Gmail configuration
+
+@emph{Gmail} is a popular e-mail provider; let's see how we can make it
+work with @t{mu4e}. Since we are using @abbr{IMAP}, you must enable that
+in the Gmail web interface (in the settings, under the ``Forwarding and
+POP/IMAP''-tab).
+
+Gmail users may also be interested in @ref{Including related messages},
+and in @ref{Skipping duplicates}.
+
+@subsection Setting up offlineimap
+
+First of all, we need a program to get the e-mail from Gmail to our
+local machine; for this we use @t{offlineimap}; on Debian (and
+derivatives like Ubuntu), this is as easy as:
+
+@verbatim
+$ sudo apt-get install offlineimap
+@end verbatim
+
+while on Fedora (and similar) you need:
+@verbatim
+$ sudo yum install offlineimap
+@end verbatim
+
+Then, we can configure @t{offlineimap} by editing @file{~/.offlineimaprc}:
+
+@verbatim
+[general]
+accounts = Gmail
+maxsyncaccounts = 3
+
+[Account Gmail]
+localrepository = Local
+remoterepository = Remote
+
+[Repository Local]
+type = Maildir
+localfolders = ~/Maildir
+
+[Repository Remote]
+type = IMAP
+remotehost = imap.gmail.com
+remoteuser = USERNAME@gmail.com
+remotepass = PASSWORD
+ssl = yes
+maxconnections = 1
+@end verbatim
+
+Obviously, you need to replace @t{USERNAME} and @t{PASSWORD} with your actual
+Gmail username and password. After this, you should be able to download your
+mail:
+
+@verbatim
+$ offlineimap
+ OfflineIMAP 6.3.4
+Copyright 2002-2011 John Goerzen & contributors.
+Licensed under the GNU GPL v2+ (v2 or any later version).
+
+Account sync Gmail:
+ ***** Processing account Gmail
+ Copying folder structure from IMAP to Maildir
+ Establishing connection to imap.gmail.com:993.
+Folder sync [Gmail]:
+ Syncing INBOX: IMAP -> Maildir
+ Syncing [Gmail]/All Mail: IMAP -> Maildir
+ Syncing [Gmail]/Drafts: IMAP -> Maildir
+ Syncing [Gmail]/Sent Mail: IMAP -> Maildir
+ Syncing [Gmail]/Spam: IMAP -> Maildir
+ Syncing [Gmail]/Starred: IMAP -> Maildir
+ Syncing [Gmail]/Trash: IMAP -> Maildir
+Account sync Gmail:
+ ***** Finished processing account Gmail
+@end verbatim
+
+We can now run @command{mu} to make sure things work:
+
+@verbatim
+$ mu index
+mu: indexing messages under /home/foo/Maildir [/home/foo/.cache/mu/xapian]
+| processing mail; checked: 520; updated/new: 520, cleaned-up: 0
+mu: elapsed: 3 second(s), ~ 173 msg/s
+mu: cleaning up messages [/home/foo/.cache/mu/xapian]
+/ processing mail; checked: 520; updated/new: 0, cleaned-up: 0
+mu: elapsed: 0 second(s)
+@end verbatim
+
+We can run both the @t{offlineimap} and the @t{mu index} from within
+@t{mu4e}, but running it from the command line makes it a bit easier to
+troubleshoot as we are setting things up.
+
+Note: when using encryption, you probably do @emph{not} want to
+synchronize your Drafts-folder, since it contains the unencrypted
+messages. You can use OfflineIMAP's @t{folderfilter} for that.
+
+@subsection Settings
+
+Next step: let's make a @t{mu4e} configuration for this:
+
+@lisp
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+(setq mu4e-drafts-folder "/[Gmail].Drafts")
+(setq mu4e-sent-folder "/[Gmail].Sent Mail")
+(setq mu4e-trash-folder "/[Gmail].Trash")
+
+;; don't save message to Sent Messages, Gmail/IMAP takes care of this
+(setq mu4e-sent-messages-behavior 'delete)
+
+;; (See the documentation for `mu4e-sent-messages-behavior' if you have
+;; additional non-Gmail addresses and want assign them different
+;; behavior.)
+
+;; setup some handy shortcuts
+;; you can quickly switch to your Inbox -- press ``ji''
+;; then, when you want archive some messages, move them to
+;; the 'All Mail' folder by pressing ``ma''.
+
+(setq mu4e-maildir-shortcuts
+ '( (:maildir "/INBOX" :key ?i)
+ (:maildir "/[Gmail].Sent Mail" :key ?s)
+ (:maildir "/[Gmail].Trash" :key ?t)
+ (:maildir "/[Gmail].All Mail" :key ?a)))
+
+;; allow for updating mail using 'U' in the main view:
+(setq mu4e-get-mail-command "offlineimap")
+
+;; something about ourselves
+(setq
+ user-mail-address "USERNAME@@gmail.com"
+ user-full-name "Foo X. Bar"
+ mu4e-compose-signature
+ (concat
+ "Foo X. Bar\n"
+ "http://www.example.com\n"))
+
+;; sending mail -- replace USERNAME with your gmail username
+;; also, make sure the gnutls command line utils are installed
+;; package 'gnutls-bin' in Debian/Ubuntu
+
+(require 'smtpmail)
+(setq message-send-mail-function 'smtpmail-send-it
+ starttls-use-gnutls t
+ smtpmail-starttls-credentials '(("smtp.gmail.com" 587 nil nil))
+ smtpmail-auth-credentials
+ '(("smtp.gmail.com" 587 "USERNAME@@gmail.com" nil))
+ smtpmail-default-smtp-server "smtp.gmail.com"
+ smtpmail-smtp-server "smtp.gmail.com"
+ smtpmail-smtp-service 587)
+
+;; alternatively, for emacs-24 you can use:
+;;(setq message-send-mail-function 'smtpmail-send-it
+;; smtpmail-stream-type 'starttls
+;; smtpmail-default-smtp-server "smtp.gmail.com"
+;; smtpmail-smtp-server "smtp.gmail.com"
+;; smtpmail-smtp-service 587)
+
+;; don't keep message buffers around
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+And that's it --- put the above in your @file{~/.emacs}, change @t{USERNAME}
+etc.@: to your own, and restart Emacs, and run @kbd{M-x mu4e}.
+
+@node CONF Other settings
+@section Other settings
+
+Finally, here are some more settings that are useful, but not enabled by
+default for various reasons.
+
+@lisp
+;; use 'fancy' non-ascii characters in various places in mu4e
+(setq mu4e-use-fancy-chars t)
+
+;; save attachment to my desktop (this can also be a function)
+(setq mu4e-attachment-dir "~/Desktop")
+
+;; attempt to show images when viewing messages
+(setq mu4e-view-show-images t)
+@end lisp
+
+@node FAQ
+@appendix FAQ --- Frequently Asked Questions
+
+In this chapter we list a number of actual and anticipated questions and their
+answers.
+
+@menu
+* General::General questions and answers about @t{mu4e}
+* Retrieving mail::Getting mail and indexing
+* Reading messages::Dealing with incoming messages
+* Writing messages::Dealing with outgoing messages
+* Known issues::Limitations we know about
+@end menu
+
+@node General
+@section General
+
+@subsection Results from @t{mu} and @t{mu4e} differ - why?
+@anchor{mu-mu4e-differ} In general, the same queries for @command{mu}
+and @t{mu4e} should yield the same results. If they differ, this is
+usually because one of the following reasons:
+@itemize
+@item different options:
+@t{mu4e} defaults to having @t{mu4e-headers-include-related}, and
+@t{mu4e-headers-results-limit} set to 500. However, the command-line
+@command{mu find}'s corresponding @t{--include-related} is false, and
+there's no limit (@t{--maxnum}).
+@item reverse sorting:
+The results may be different when @t{mu4e} and @command{mu find} do
+not both sort their results in the same direction.
+@item shell quoting issues:
+Depending on the shell, various shell metacharacters in search query
+(such as @t{*}) may be expanded by the shell before @command{mu} ever
+sees them, and the query may not be what you think it is. Quoting is
+necessary.
+@end itemize
+
+@subsection The unread/all counts in the main-screen differ from the 'real' numbers - what's going on?
+For speed reasons, the counts do not exclude messages that no longer
+exist in the file-system, nor does it exclude duplicate messages; @xref{mu-mu4e-differ}.
+
+@subsection How can I quickly delete/move/trash a lot of messages?
+You can select ('mark' in Emacs-speak) the messages like you would
+select text in a buffer; the actions you then take (e.g., @key{DEL}
+for delete, @key{m} for move and @key{t} for trash) apply to all
+selected messages. You can also use functions like
+@code{mu4e-headers-mark-thread} (@key{T}),
+@code{mu4e-headers-mark-subthread} (@key{t}) to mark whole threads at
+the same time, and @code{mu4e-headers-mark-pattern} (@key{%}) to mark
+all messages matching a certain regular expression.
+
+@subsection Can I automatically apply the marks on messages when leaving the headers buffer?
+Yes you can --- see the documentation for the variable
+@t{mu4e-headers-leave-behavior}.
+
+@subsection How can I set @t{mu4e} as the default e-mail client in Emacs?
+See @ref{Emacs default}.
+
+@subsection Can @t{mu4e} use some fancy Unicode instead of these boring plain-ASCII ones?
+Glad you asked! Yes, if you set @code{mu4e-use-fancy-chars} to @t{t},
+@t{mu4e} uses such fancy characters in a number of places. Since not
+all fonts include all characters, you may want to install the
+@t{unifont} and/or @t{symbola} fonts on your system.
+
+@subsection Can I start @t{mu4e} in the background?
+Yes --- if you provide a prefix-argument (@key{C-u}), @t{mu4e} starts,
+but does not show the main-window.
+
+@subsection Does @t{mu4e} support searching for CJK (Chinese-Japanese-Korean) characters?
+Yes, if you have @t{Xapian} 1.2.8 or newer, and set the environment
+variable @t{XAPIAN_CJK_NGRAM} to non-empty before indexing, both when
+using @t{mu} from the command-line and from @t{mu4e}.
+
+@subsection How can I customize the function to select a folder?
+The @t{mu4e-completing-read-function} variable can be customized to
+select a folder in any way. The variable can be set to a function that
+receives five arguments, following @t{completing-read}. The default
+value is @code{ido-completing-read}; to use emacs's default behavior,
+set the variable to @code{completing-read}. Helm users can use the
+same value, and by enabling @code{helm-mode} use helm-style
+completion.
+
+@subsection With a lot of Maildir folders, jumping to them can get slow. What can I do?
+Set @code{mu4e-cache-maildir-list} to @code{t} (make sure to read
+its docstring).
+
+@subsection How can I hide messages from the search results?
+See the variable @code{mu4e-headers-hide-predicate}.
+
+For example, to filter out GMail's spam, set it to:
+@lisp
+(setq mu4e-headers-hide-predicate
+ (lambda (msg)
+ (string-suffix-p "Spam" (mu4e-message-field msg :maildir))))
+@end lisp
+
+@subsection I'm getting an error 'Variable binding depth exceeds max-specpdl-size' when using mu4e -- what can I do about it?
+The error occurs because @t{mu4e} is binding more variables than
+@t{emacs} allows for, by default. You can avoid this by setting a
+higher value, e.g. by adding the following to your configuration:
+@lisp
+(setq max-specpdl-size 5000)
+@end lisp
+
+@node Retrieving mail
+@section Retrieving mail
+
+@subsection How can I get notifications when receiving mail?
+There is @code{mu4e-index-updated-hook}, which gets triggered when the
+indexing process triggered sees an update (not just new mail though). To
+use this hook, put something like the following in your setup (assuming
+you have @t{aplay} and some soundfile, change as needed):
+@lisp
+(add-hook 'mu4e-index-updated-hook
+ (defun new-mail-sound ()
+ (shell-command "aplay ~/Sounds/boing.wav&")))
+@end lisp
+
+@subsection Getting mail through a local mailserver. What should I use for @code{mu4e-get-mail-command}?
+Use the literal string @t{"true"} (or don't do anything, it's the
+default) which then uses @t{/bin/true} (a command that does nothing and
+always succeeds). This makes getting mail a no-op, but the messages are
+still re-indexed.
+
+@subsection How can I re-index my messages without getting new mail?
+Use @kbd{M-x mu4e-update-index}
+
+@subsection When I try to run @t{mu index} while @t{mu4e} is running I get errors
+For instance:
+@verbatim
+mu: mu_store_new_writable: xapian error
+ 'Unable to get write lock on ~/.cache/mu/xapian: already locked
+@end verbatim
+What to do about this? You get this error because the underlying Xapian
+database is locked by some other process; it can be opened only once in
+read-write mode. There is not much @t{mu4e} can do about this, but if is
+another @command{mu} instance that is holding the lock, you can ask it
+to (gracefully) terminate:
+@verbatim
+ pkill -2 -u $UID mu # send SIGINT
+ sleep 1
+ mu index
+@end verbatim
+@t{mu4e} automatically restarts @t{mu} when it needs it. In practice, this
+seems to work quite well.
+
+@subsection How can I disable the @t{Indexing...} messages?
+Set the variable @code{mu4e-hide-index-messages} to non-@t{nil}.
+
+@subsection IMAP-synchronization and file-name changes
+Some IMAP-synchronization programs such as @t{mbsync} (but not
+@t{offlineimap}) don't like it when message files do not change their
+names when they are moved to different folders. @t{mu4e} can attempt to
+help with this - you can set the variable
+@code{mu4e-change-filenames-when-moving} to non-@t{nil}.
+
+@subsection @command{offlineimap} and UTF-7
+@command{offlineimap} uses IMAP's UTF-7 for encoding non-ascii folder
+names, while @command{mu} expects UTF-8 (so, e.g. @t{/まりもえ
+お}@footnote{some Japanese characters} becomes @t{/&MH4wijCCMEgwSg-}).
+
+This is best solved by telling @command{offlineimap} to use UTF-8
+instead --- see
+@url{https://github.com/djcb/mu/issues/68#issuecomment-8598652}.
+
+@subsection @command{mbsync} or @command{offlineimap} do not sync properly
+Unfortunately, @command{mbsync} and/or @command{offlineimap} do not
+always agree with @t{mu} about the meaning of various Maildir-flags. If
+you encounter unexpected behavior, it is recommended you check before
+and after a sync-operation. If the problem only shows up @emph{after}
+sync'ing, the problem is with the sync-program, and it's most productive
+to complain there.
+
+Also, you may want to ensure that @t{mu4e-index-lazy-check} is kept at
+its default (@t{nil}) value, since it seems @command{mbsync} can make
+changes that escape a 'lazy' check.
+
+Furthermore, there have been quite a few related queries on the
+mailing-list; worthwhile to check out.
+
+@node Reading messages
+@section Reading messages
+
+@subsection Opening messages is slower than expected - why?
+@t{mu4e} is designed to be very fast, even with large amounts of mail.
+However, if you experience slowdowns, here are some things to consider:
+@itemize
+@item opening messages while indexing:
+@t{mu4e} corresponds with the @t{mu} server synchronously; this means
+that you can do only one thing at a time. The one operation that
+potentially does take a bit of time is indexing of mail. Since the 1.7.x
+series you don't have to wait for the indexing to complete, but it can
+be still be a bit slower because @t{mu} is very busy at that time.
+
+For some strategies to reduce that time, see the next question.
+@item getting contact information can take some time:
+especially when opening @t{mu4e} the first time and you have a
+@emph{lot} of contacts, it can take a few seconds to process those. Note
+that @t{mu4e} 1.3 and higher only get @emph{changed} contacts in
+subsequent updates (after and indexing operation), so this should be
+less of a concern. And you can tweak what contacts you get using
+@var{mu4e-compose-complete-only-personal},
+@var{mu4e-compose-complete-only-after} and
+@var{mu4e-compose-complete-max}.
+@item decryption / sign verification:
+encrypted / signed messages sometimes require network access, and this
+may take a while; certainly if the needed servers cannot be found.
+Part of this may be that influential environment variables are not set
+in the emacs environment.
+@end itemize
+
+If you still experience unexpected slowness, you can of course file a
+ticket, but please be sure to mention the following:
+
+@itemize
+@item are all messages slow or only some messages?
+@item if it's only some messages, is there something specific about them?
+@item in addition, please a (sufficiently censored version of) a message that is slow
+@item is opening @emph{always} slow or only sometimes? When?
+@end itemize
+
+@subsection How can I word-wrap long lines in when viewing a message?
+You can toggle between wrapped and non-wrapped states using
+@key{w}. If you want to do this automatically, invoke @code{visual-line-mode} in
+your @code{mu4e-view-mode-hook}.
+@subsection How can I perform custom actions on messages and attachments?
+See @ref{Actions}.
+@subsection How can I prevent @t{mu4e} from automatically marking messages as `read' when I read them?
+Set @code{mu4e-view-auto-mark-as-read} to @code{nil}.
+@subsection Does @t{mu4e} support including all related messages in a thread, like Gmail does?
+Yes --- see @ref{Including related messages}.
+@subsection There seems to be a lot of duplicate messages --- how can I get rid of them?
+See @ref{Skipping duplicates}.
+@subsection Some messages are almost unreadable in emacs --- can I view them in an external web browser?
+Indeed, airlines often send messages that heavily depend on html and
+are hard to digest inside emacs. Fortunately, there's an @emph{action}
+(@ref{Message view actions}) defined for this. Simply add to your
+configuration:
+@lisp
+(add-to-list 'mu4e-view-actions
+ '("ViewInBrowser" . mu4e-action-view-in-browser) t)
+@end lisp
+Now, when viewing such a difficult message, type @kbd{aV}, and the
+message opens inside a web browser. You can influence the browser to
+use with @code{browse-url-generic-program}.
+@subsection How can I read encrypted messages that I sent?
+Since you do not own the recipient's key you typically cannot read
+those mails --- so the trick is to encrypt outgoing mails with your
+key, too. This can be automated by adding the following snippet to
+your configuration (courtesy of user @t{kpachnis}):
+@lisp
+(require 'epg-config)
+(setq mml2015-use 'epg
+ epg-user-id "gpg_key_id"
+ mml2015-encrypt-to-self t
+ mml2015-sign-with-sender t)
+@end lisp
+@subsection Can I `bounce' or `resend' messages?
+Somewhat --- it is possible to edit a (copy of) an existing message and
+then send it, using @code{M-x mu4e-compose-resend}. This gives you a
+raw copy of the message, including all headers, encoded parts and so
+on. Reason for this is that for resending, it is important not to
+change anything (except perhaps for the @t{To:} address when
+bouncing); since we cannot losslessly decode an existing message, you
+get the raw version.
+
+@node Writing messages
+@section Writing messages
+
+@subsection What's the deal with replies to messages I wrote myself?
+Like many other mail-clients, @t{mu4e} treats replies to messages you
+wrote yourself as special --- these messages keep the same @t{To:} and
+@t{Cc:} as the original message. This is to ease the common case of
+following up to a message you wrote earlier.
+
+@subsection How can I automatically set the @t{From:}-address for a reply-message?
+See @ref{Compose hooks}.
+
+@subsection How can I dynamically determine the folders for draft/sent/trashed messages?
+See @ref{Dynamic folders}.
+
+@subsection How can I define aliases for (groups of) e-mail addresses?
+See @ref{(emacs) Mail Aliases}.
+
+@subsection How can I automatically add some header to an outgoing message?
+See @ref{Compose hooks}.
+
+@subsection How can I influence the way the original message looks when replying/forwarding?
+Since @code{mu4e-compose-mode} derives from @code{message-mode}, you can
+re-use many of its facilities.
+
+@subsection How can I easily include attachments in the messages I write?
+You can drag-and-drop from your desktop; alternatively, you can use
+@t{dired} --- see @ref{Dired}.
+
+@subsection @t{mu4e} seems to remove myself from the @t{Cc:}-list; how can I prevent that?
+Set @code{mu4e-compose-keep-self-cc} to @t{t} in your configuration.
+
+@subsection @t{mu4e} include myself from the @t{Cc:}-list; how can I prevent that?
+You need list your personal addresses by passing one or more
+@t{--my-address=...} to @t{mu init}. Note that the
+@code{mu4e-user-mail-address-list} which was used in older @t{mu4e}
+versions is no longer used. Also see the entries for version 1.4 in
+@t{NEWS.org} (@kbd{N}) in the main-menu.
+
+@subsection How can I start a new message-thread from a reply?
+Remove the @t{In-Reply-To} header, and @t{mu4e} automatically removes
+the (hidden) @t{References} header as well when sending it. This makes
+the message show up as a top-level message rather than as a response.
+
+@subsection How can I attach an existing message?
+Use @code{mu4e-action-capture-message} (i.e., @kbd{a c} in the headers
+ view) to `capture' the to-be-attached message, then when editing the
+ message, use @kbd{M-x mu4e-compose-attach-captured-message}.
+
+@subsection How can I sign or encrypt messages?
+You can do so using Emacs' MIME-support --- check the
+@t{Attachments}-menu while composing a message. Also see @ref{Signing
+and encrypting}.
+
+You can do so using Emacs' MIME-support --- check the
+@t{Attachments}-menu while composing a message. Also see @ref{Signing
+and encrypting}.
+
+@subsection Address auto-completion does not work?
+If you have set @code{mu4e-compose-complete-only-personal} to non-nil,
+@t{mu4e} only completes 'personal' addresses - so you tell it about your
+e-mail addresses when setting up the database (@t{mu init});
+@ref{Initializing the message store}.
+
+If you cannot find specific addresses you'd expect to find, inspect the
+values of @var{mu4e-compose-complete-only-personal},
+@var{mu4e-compose-complete-only-after} and
+@var{mu4e-compose-complete-max}.
+
+@subsection Can I use @t{BBDB} with @t{mu4e}?
+Yes, with releases of BBDB after 3.1.2. @ref{BBDB}.
+
+@subsection How can I get rid of the message buffer after sending?
+@lisp
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+@subsection Sending big messages is slow and blocks emacs --- what can I do about it?
+
+For this, there's @url{https://github.com/jwiegley/emacs-async} (also
+available from the Emacs package repository); add the following snippet
+to your configuration:
+@lisp
+(require 'smtpmail-async)
+(setq
+ send-mail-function 'async-smtpmail-send-it
+ message-send-mail-function 'async-smtpmail-send-it)
+@end lisp
+With this, messages are sent using a background Emacs instance.
+
+A word of warning though, this tends to not be as reliable as sending
+the message in the normal, synchronous fashion, and people have reported
+silent failures, where mail sending fails for some reason without any
+indication of that.
+
+You can check the progress of the background by checking the
+@t{*Messages*}-buffer, which should show something like:
+@verbatim
+Delivering message to "William Shakespeare" <will@example.com>...
+Mark set
+Saving file /home/djcb/Maildir/sent/cur/20130706-044350-darklady:2,S...
+Wrote /home/djcb/Maildir/sent/cur/20130706-044350-darklady:2,S
+Sending...done
+@end verbatim
+The first and final messages are the most important, and there may be
+considerable time between them, depending on the size of the message.
+
+@subsection Is it possible to compose messages in a separate frame?
+Yes --- set the variable @code{mu4e-compose-in-new-frame} to @code{t}.
+
+@subsection How can I apply format=flowed to my outgoing messages?
+This enables receiving clients that support this feature to reflow
+paragraphs. Plain text emails with @t{Content-Type: text/plain;
+format=flowed} can be reflowed (i.e. line endings removed, paragraphs
+refilled) by receiving clients that support this standard. Clients
+that don't support this, show them as is, which means this feature is
+truly non-invasive.
+
+Here's an explanatory blog post which also shows why this is a
+desirable feature:
+@url{https://mathiasbynens.be/notes/gmail-plain-text} (if you don't
+have it, your mails mostly look quite bad especially on mobile
+devices) and here's the RFC with all the details:
+@url{https://www.ietf.org/rfc/rfc2646.txt}.
+
+Since version 0.9.17, @t{mu4e} sends emails with @t{format=flowed} by
+setting
+@lisp
+(setq mu4e-compose-format-flowed t)
+@end lisp
+
+@noindent
+in your Emacs init file (@file{~/.emacs} or @file{~/.emacs.d/init.el}).
+The transformation of your message into the proper format is done at the
+time of sending. For this to happen properly, you should write each
+paragraph of your message of as a long line (i.e. without carriage
+return). If you introduce unwanted newlines in your paragraph, use
+@kbd{M-q} to reformat it as a single line.
+
+If you want to send the message with paragraphs on single lines but
+without @t{format=flowed} (because, say, the receiver does not
+understand the latter as it is the case for Google or Github), use
+@kbd{M-x use-hard-newlines} (to turn @code{use-hard-newlines} off) or
+uncheck the box @t{format=flowed} in the @t{Text} menu when composing a
+message.
+
+@subsection How can I force images to be shown at the end of my messages, regardless of where I insert them?
+User Marcin Borkowski has a solution:
+@lisp
+(defun mml-attach-file--go-to-eob (orig-fun &rest args)
+ "Go to the end of buffer before attaching files."
+ (save-excursion
+ (save-restriction
+ (widen)
+ (goto-char (point-max))
+ (apply orig-fun args))))
+
+(advice-add 'mml-attach-file :around #'mml-attach-file--go-to-eob)
+@end lisp
+
+@subsection How can I avoid Outlook display issues?
+
+Limited testing shows that certain Outlook clients do not work well with
+inline replies, and the entire message including-and-below the first
+quoted section is collapsed. This means recipients may not even notice
+important inline text, especially if there is some top-posted content.
+This has been observed on OS X, Windows, and Web-based Outlook clients
+accessing Office 365.
+
+It appears the bug is triggered by the standard reply regex "On ...
+wrote:". Changing "On", or removing the trailing ":" appears to fix the
+bug (in limited testing). Therefore, a simple work-around is to set
+`message-citation-line-format` to something slightly non-standard, such
+as:
+@lisp
+(setq message-citation-line-format "On %Y-%m-%d at %R %Z, %f wrote...")
+@end lisp
+
+@node Known issues
+@section Known issues
+
+Although they are not really @emph{questions}, we end this chapter with
+a list of known issues and/or missing features in @t{mu4e}. Thus, users
+won't have to search in vain for things that are not there (yet), and
+the author can use it as a todo-list.
+
+@subsection UTF-8 language environment is required
+@t{mu4e} does not work well if the Emacs language environment is not
+UTF-8; so, if you encounter problems with encodings, be sure to have
+@code{(set-language-environment "UTF-8")} in your @file{~/.emacs} (or
+its moral equivalents in other places).
+
+@subsection Thread handling is incomplete
+While threads are calculated and are visible in the headers buffer,
+you cannot collapse/open them.
+
+@subsection Key-bindings are @emph{somewhat} hard-coded.
+That is, the main menu assumes the default key-bindings, as do the
+clicks-on-bookmarks.
+
+For a more complete list, please refer to the issues-list in the
+github-repository.
+
+@node Tips and Tricks
+@appendix Tips and Tricks
+
+@menu
+* Fancy characters:: Non-ascii characters in the UI
+* Refiling messages:: Moving message to some archive folder
+* Saving outgoing messages:: Automatically save sent messages
+* Confirmation before sending:: Check messages before sending
+@end menu
+
+@node Fancy characters
+@section Fancy characters
+
+When using `fancy characters' (@code{mu4e-use-fancy-chars}) with the
+@emph{Inconsolata}-font (and likely others as well), the display may be
+slightly off; the reason for this issue is that Inconsolata does not
+contain the glyphs for the `fancy' arrows and the glyphs that are used
+as replacements are too high.
+
+To fix this, you can use something like the following workaround (in
+your @t{.emacs}-file):
+@lisp
+(when (equal window-system 'x)
+ (set-fontset-font "fontset-default" 'unicode "Dejavu Sans Mono")
+ (set-face-font 'default "Inconsolata-10"))
+@end lisp
+
+Other fonts with good support for Unicode are @t{unifont} and
+@t{symbola}.
+
+For a more complete solution, but with greater overhead, you can also
+try the @emph{unicode-fonts} package:
+@lisp
+(require 'unicode-fonts)
+(require 'persistent-soft) ; To cache the fonts and reduce load time
+(unicode-fonts-setup)
+@end lisp
+
+@node Refiling messages
+@section Refiling messages
+
+By setting @code{mu4e-refile-folder} to a function, you can dynamically
+determine where messages are to be refiled. If you want to do this based
+on the subject of a message, you can use a function that matches the
+subject against a list of regexes in the following way. First, set up a
+variable @code{my-mu4e-subject-alist} containing regexes plus associated
+mail folders:
+
+@lisp
+(defvar my-mu4e-subject-alist '(("kolloqui\\(um\\|a\\)" . "/Kolloquium")
+ ("Calls" . "/Calls")
+ ("Lehr" . "/Lehre")
+ ("webseite\\|homepage\\|website" . "/Webseite"))
+ "List of subjects and their respective refile folders.")
+@end lisp
+
+Now you can use the following function to automatically refile messages
+based on their subject line:
+
+@lisp
+(defun my-mu4e-refile-folder-function (msg)
+ "Set the refile folder for MSG."
+ (let ((subject (mu4e-message-field msg :subject))
+ (folder (or (cdar (member* subject my-mu4e-subject-alist
+ :test #'(lambda (x y)
+ (string-match (car y) x))))
+ "/General")))
+ folder))
+@end lisp
+
+Note the @t{"/General"} folder: it is the default folder in case the
+subject does not match any of the regexes in
+@code{my-mu4e-subject-alist}.
+
+In order to make this work, you'll of course need to set
+@code{mu4e-refile-folder} to this function:
+
+@lisp
+(setq mu4e-refile-folder 'my-mu4e-refile-folder-function)
+@end lisp
+
+If you have multiple accounts, you can accommodate them as well:
+
+@lisp
+(defun my-mu4e-refile-folder-function (msg)
+ "Set the refile folder for MSG."
+ (let ((maildir (mu4e-message-field msg :maildir))
+ (subject (mu4e-message-field msg :subject))
+ folder)
+ (cond
+ ((string-match "Account1" maildir)
+ (setq folder (or (catch 'found
+ (dolist (mailing-list my-mu4e-mailing-lists)
+ (if (mu4e-message-contact-field-matches
+ msg :to (car mailing-list))
+ (throw 'found (cdr mailing-list)))))
+ "/Account1/General")))
+ ((string-match "Gmail" maildir)
+ (setq folder "/Gmail/All Mail"))
+ ((string-match "Account2" maildir)
+ (setq folder (or (cdar (member* subject my-mu4e-subject-alist
+ :test #'(lambda (x y)
+ (string-match
+ (car y) x))))
+ "/Account2/General"))))
+ folder))
+@end lisp
+
+This function actually uses different methods to determine the refile
+folder, depending on the account: for @emph{Account2}, it uses
+@code{my-mu4e-subject-alist}, for the @emph{Gmail} account it simply uses the
+folder ``All Mail''. For Account1, it uses another method: it files the
+message based on the mailing list to which it was sent. This requires
+another variable:
+
+@lisp
+(defvar my-mu4e-mailing-lists
+ '(("mu-discuss@@googlegroups.com" . "/Account1/mu4e")
+ ("pandoc-discuss@@googlegroups.com" . "/Account1/Pandoc")
+ ("auctex@@gnu.org" . "/Account1/AUCTeX"))
+ "List of mailing list addresses and folders where
+ their messages are saved.")
+@end lisp
+
+@node Saving outgoing messages
+@section Saving outgoing messages
+
+Like @code{mu4e-refile-folder}, the variable @code{mu4e-sent-folder} can
+also be set to a function, in order to dynamically determine the save
+folder. One might, for example, wish to automatically put messages going
+to mailing lists into the trash (because you'll receive them back from
+the list anyway). If you have set up the variable
+@code{my-mu4e-mailing-lists} as mentioned, you can use the following
+function to determine a 'sent'-folder:
+
+@lisp
+(defun my-mu4e-sent-folder-function (msg)
+ "Set the sent folder for the current message."
+ (let ((from-address (message-field-value "From"))
+ (to-address (message-field-value "To")))
+ (cond
+ ((string-match "my.address@@account1.example.com" from-address)
+ (if (member* to-address my-mu4e-mailing-lists
+ :test #'(lambda (x y)
+ (string-match (car y) x)))
+ "/Trash"
+ "/Account1/Sent"))
+ ((string-match "my.address@@gmail.com" from-address)
+ "/Gmail/Sent Mail")
+ (t (mu4e-ask-maildir-check-exists "Save message to maildir: ")))))
+@end lisp
+
+Note that this function doesn't use @code{(mu4e-message-field msg
+:maildir)} to determine which account the message is being sent from.
+The reason is that the function in @code{mu4e-sent-folder} is
+called when you send the message, but before @t{mu4e} has created the
+message struct from the compose buffer, so that
+@code{mu4e-message-field} cannot be used. Instead, the function uses
+@code{message-field-value}, which extracts the values of the headers in
+the compose buffer. This means that it is not possible to extract the
+account name from the message's maildir, so instead the from address is
+used to determine the account.
+
+Again, the function shows three different possibilities: for the first
+account (@t{my.address@@account1.example.com}) it uses
+@code{my-mu4e-mailing-lists} again to determine if the message goes to a
+mailing list. If so, the message is put in the trash folder, if not, it
+is saved in @t{/Account1/Sent}. For the second (Gmail) account, sent
+mail is simply saved in the Sent Mail folder.
+
+If the from address is not associated with Account1 or with the Gmail
+account, the function uses @code{mu4e-ask-maildir-check-exists} to ask
+the user for a maildir to save the message in.
+
+@node Confirmation before sending
+@section Confirmation before sending
+
+To protect yourself from sending messages too hastily, you can add a
+final confirmation, which you can of course make as elaborate as you
+wish.
+
+@lisp
+(add-hook 'message-send-hook
+ (lambda ()
+ (unless (yes-or-no-p "Sure you want to send this?")
+ (signal 'quit nil))))
+@end lisp
+
+Another option is to simply set @code{message-confirm-send} to
+non-@t{nil} so the question ``Send message?'' is asked for confirmation.
+
+@node How it works
+@appendix How it works
+
+While perhaps not interesting for all users of @t{mu4e}, some curious
+souls may want to know how @t{mu4e} does its job.
+
+@menu
+* High-level overview::How the pieces fit together
+* mu server::The mu process running in the background
+* Reading from the server::Processing responses from the server
+* The message s-expression::What messages look like from the inside
+@end menu
+
+@node High-level overview
+@section High-level overview
+
+At a high level, we can summarize the structure of the @t{mu4e} system using
+some ascii-art:
+
+@cartouche
+@example
+ +---------+
+ | emacs |
+ | +------+
+ +----| mu4e | --> send mail (smtpmail)
+ +------+
+ | A
+ V | ---/ search, view, move mail
+ +---------+ \
+ | mu |
+ +---------+
+ | A
+ V |
+ +---------+
+ | Maildir | <--- receive mail (fetchmail,
+ +---------+ offlineimap, ...)
+@end example
+@end cartouche
+
+In words:
+@itemize
+@item Your e-mail messages are stored in a Maildir-directory
+(typically, @file{~/Maildir} and its subdirectories), and new mail comes in
+using tools like @t{fetchmail}, @t{offlineimap}, or through a local mail
+server.
+@item @t{mu} indexes these messages periodically, so you can quickly search for
+them. @t{mu} can run in a special @t{server}-mode, where it provides services
+ to client software.
+@item @t{mu4e}, which runs inside Emacs is
+ such a client; it communicates with @command{mu} (in its @t{server}-mode) to
+ search for messages, and manipulate them.
+@item @t{mu4e} uses the facilities
+ offered by Emacs (the Gnus message editor and @t{smtpmail}) to send
+ messages.
+@end itemize
+
+@node mu server
+@section @t{mu server}
+
+@t{mu4e} is based on the @t{mu} e-mail searching/indexer. The latter
+is a C++-program; there are different ways to communicate with a
+client that is emacs-based.
+
+One way to implement this, would be to call the @t{mu} command-line
+tool with some parameters and then parse the output. In fact, that was
+the first approach --- @t{mu4e} would invoke e.g., @t{mu find} and
+process the output in Emacs.
+
+However, with this approach, we need to load the entire e-mail
+@emph{Xapian} database (in which the message is stored) for each
+invocation. Wouldn't it be nicer to keep a running @t{mu} instance
+around? Indeed, it would --- and thus, the @t{mu server} sub-command
+was born. Running @t{mu server} starts a simple shell, in which you
+can give commands to @command{mu}, which then spits out the
+results/errors. @command{mu server} is not meant for humans, but it
+can be used manually, which is great for debugging.
+
+@node Reading from the server
+@section Reading from the server
+
+In the design, the next question was what format @t{mu} should use for its
+output for @t{mu4e} (Emacs) to process. Some other programs use
+@abbr{JSON} here, but it seemed easier (and possibly, more efficient) just to
+talk to Emacs in its native language: @emph{s-expressions}, and
+interpret those using the Emacs-function
+@code{read-from-string}. See @ref{The message s-expression} for details on the
+format.
+
+So, now let's look at how we process the data from @t{mu server} in
+Emacs. We'll leave out a lot of details, @t{mu4e}-specifics, and look
+at a bit more generic approach.
+
+The first thing to do is to create a process (for example, with
+@code{start-process}), and then register a filter function for it, which is
+invoked whenever the process has some data for us. Something like:
+
+@lisp
+ (let ((proc (start-process <arguments>)))
+ (set-process-filter proc 'my-process-filter)
+ (set-process-sentinel proc 'my-process-sentinel))
+@end lisp
+
+Note, the process sentinel is invoked when the process is terminated
+--- so there you can clean things up. The function
+@code{my-process-filter} is a user-defined function that takes the
+process and the chunk of output as arguments; in @t{mu4e} it looks
+something like (pseudo-lisp):
+
+@lisp
+(defun my-process-filter (proc str)
+ ;; mu4e-buf: a global string variable to which data gets appended
+ ;; as we receive it
+ (setq mu4e-buf (concat mu4e-buf str))
+ (when <we-have-received-a-full-expression>
+ <eat-expression-from mu4e-buf>
+ <evaluate-expression>))
+@end lisp
+
+@code{<evaluate-expression>} de-multiplexes the s-expression we got.
+For example, if the s-expression looks like an e-mail message header,
+it is processed by the header-handling function, which appends it to
+the header list. If the s-expression looks like an error message, it
+is reported to the user. And so on.
+
+The language between frontend and backend is documented partly in the
+@t{mu-server} man-page and more completely in the output of @t{mu
+server --commands}.
+
+@t{mu4e} can log these communications; you can use @kbd{M-x
+mu4e-toggle-logging} to turn logging on and off, and you can view the
+log using @kbd{M-x mu4e-show-log} (@key{$}).
+
+@node The message s-expression
+@section The message s-expression
+
+As a word of warning, the details of the s-expression are internal to
+the mu4e - mu communications, and are subject to change between
+versions.
+
+A typical message s-expression looks something like the following:
+
+@lisp
+(:docid 32461
+ :from ((:name "Nikola Tesla" :email "niko@@example.com"))
+ :to ((:name "Thomas Edison" :email "tom@@example.com"))
+ :cc ((:name "Rupert The Monkey" :email "rupert@@example.com"))
+ :subject "RE: what about the 50K?"
+ :date (20369 17624 0)
+ :size 4337
+ :message-id "C8233AB82D81EE81AF0114E4E74@@123213.mail.example.com"
+ :path "/home/tom/Maildir/INBOX/cur/133443243973_1.10027.atlas:2,S"
+ :maildir "/INBOX"
+ :priority normal
+ :flags (seen attach)
+ ....
+")
+@end lisp
+
+This s-expression forms a property list (@t{plist}), and we can get
+values from it using @t{plist-get}; for example @code{(plist-get msg
+:subject)} would get you the message subject. However, it's better to
+use the function @code{mu4e-message-field} to shield you from some of
+the implementation details that are subject to change; and see the other
+convenience functions in @file{mu4e-message.el}.
+
+Some notes on the format:
+@itemize
+@item The address fields are @emph{lists} of @t{plists} of the form @code{(:name <name> :email <email>)},
+where @t{name} can be @t{nil}.
+@item The date is in format Emacs uses (for example in
+@code{current-time}).@footnote{Emacs 32-bit integers have only 29 bits
+available for the actual number; the other bits are use by Emacs for
+internal purposes. Therefore, we need to split @t{time_t} in two
+numbers.}
+@end itemize
+
+@subsection Example: ping-pong
+
+As an example of the communication between @t{mu4e} and @command{mu},
+let's look at the @t{ping-pong}-sequence. When @t{mu4e} starts, it
+sends a command @t{ping} to the @t{mu server} backend, to learn about
+its version. @t{mu server} then responds with a @t{pong} s-expression
+to provide this information (this is implemented in
+@file{mu-cmd-server.c}).
+
+We start this sequence when @t{mu4e} is invoked (when the program is
+started). It calls @t{mu4e-server-ping}, and registers a (lambda)
+function for @t{mu4e-server-pong-func}, to handle the response.
+
+@verbatim
+-> (ping)
+<-<prefix>(:pong "mu" :props (:version "x.x.x" :doccount 78545))
+@end verbatim
+
+When we receive such a @t{pong} (in @file{mu4e-server.el}), the lambda
+function we registered is called, and it compares the version we got
+from the @t{pong} with the version we expected, and raises an error if
+they differ.
+
+@node Debugging
+@appendix Debugging
+
+As explained in @ref{How it works}, @t{mu4e} communicates with its
+backend (@t{mu server}) by sending commands and receiving responses
+(s-expressions).
+
+For debugging purposes, it can be very useful to see this data. For
+this reason, @t{mu4e} can log all these messages. Note that the
+`protocol' is documented to some extent in the @t{mu-server} manpage.
+
+You can enable (and disable) logging with @kbd{M-x
+mu4e-toggle-logging}. The log-buffer is called @t{*mu4e-log*}, and in
+the @ref{Main view}, @ref{Headers view} and @ref{Message view},
+there's a keybinding @key{$} that takes you there. You can quit it by
+pressing @key{q}.
+
+Logging can be a bit resource-intensive, so you may not want to leave
+it on all the time. By default, the log only maintains the most recent
+1200 lines. @t{mu} itself keeps a log as well, you can find it in
+@t{<MUHOME>/mu.log}, on Unix typically @t{~/.cache/mu/mu.log}.
+
+@node GNU Free Documentation License
+@appendix GNU Free Documentation License
+
+@include fdl.texi
+
+@bye
+
+@c Local Variables:
+@c coding: utf-8
+@c End:
--- /dev/null
+;;; org-mu4e -- support for links to mu4e messages and writing org-mode messages -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2019 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: outlines, hypermedia, calendar, mail
+;; Version: 0.0
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e is free software: you can redistribute 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 1the License, or
+;; (at your option) any later version.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; OBSOLETE, UNSUPPORTED.
+
+;; Support for links to mu4e messages/queries from within org-mode,
+;; and for writing message in org-mode, sending them as rich-text.
+
+;; At least version 8.x of Org mode is required.
+
+;;; Code:
+
+(require 'org)
+(require 'mu4e-compose)
+
+(declare-function mu4e-last-query "mu4e-headers")
+(declare-function mu4e-message-at-point "mu4e-message")
+(declare-function mu4e-view-message-with-message-id "mu4e-view")
+(declare-function mu4e-headers-search "mu4e-headers")
+(declare-function mu4e-error "mu4e-helpers")
+(declare-function mu4e-message "mu4e-message")
+(declare-function mu4e-compose-mode "mu4e-compose")
+
+\f
+;;; Editing with org-mode
+;;
+;; below, some functions for the org->html conversion
+;; based on / inspired by Eric Schulte's org-mime.el
+;; Homepage: http://orgmode.org/worg/org-contrib/org-mime.php
+;;
+;; EXPERIMENTAL
+
+(defvar org-export-skip-text-before-1st-heading)
+(defvar org-export-htmlize-output-type)
+(defvar org-export-preserve-breaks)
+(defvar org-export-with-LaTeX-fragments)
+
+(defun org~mu4e-mime-file (ext path id)
+ "Create a file of type EXT at PATH with ID for an attachment."
+ (format (concat "<#part type=\"%s\" filename=\"%s\" "
+ "disposition=inline id=\"<%s>\">\n<#/part>\n")
+ ext path id))
+
+(defun org~mu4e-mime-multipart (plain html &optional images)
+ "Create a multipart/alternative with PLAIN and HTML alternatives.
+If the html portion of the message includes IMAGES, wrap the html
+and images in a multipart/related part."
+ (concat "<#multipart type=alternative><#part type=text/plain>"
+ plain
+ (when images "<#multipart type=related>")
+ "<#part type=text/html>"
+ html
+ images
+ (when images "<#/multipart>\n")
+ "<#/multipart>\n"))
+
+(defun org~mu4e-mime-replace-images (str current-file)
+ "Replace images in html files STR in CURRENT-FILE with cid links."
+ (let (html-images)
+ (cons
+ (replace-regexp-in-string ;; replace images in html
+ "src=\"\\([^\"]+\\)\""
+ (lambda (text)
+ (format
+ "src=\"cid:%s\""
+ (let* ((url (and (string-match "src=\"\\([^\"]+\\)\"" text)
+ (match-string 1 text)))
+ (path (expand-file-name
+ url (file-name-directory current-file)))
+ (ext (file-name-extension path))
+ (id (replace-regexp-in-string "[\/\\\\]" "_" path)))
+ (cl-pushnew (org~mu4e-mime-file
+ (concat "image/" ext) path id)
+ html-images
+ :test 'equal)
+ id)))
+ str)
+ html-images)))
+
+(defun org~mu4e-mime-convert-to-html ()
+ "Convert the current body to html."
+ (unless (fboundp 'org-export-string-as)
+ (mu4e-error "Required function 'org-export-string-as not found"))
+ (let* ((begin
+ (save-excursion
+ (goto-char (point-min))
+ (search-forward mail-header-separator)))
+ (end (point-max))
+ (raw-body (buffer-substring begin end))
+ (tmp-file (make-temp-name (expand-file-name "mail"
+ temporary-file-directory)))
+ ;; because we probably don't want to skip part of our mail
+ (org-export-skip-text-before-1st-heading nil)
+ ;; because we probably don't want to export a huge style file
+ (org-export-htmlize-output-type 'inline-css)
+ ;; makes the replies with ">"s look nicer
+ (org-export-preserve-breaks t)
+ ;; dvipng for inline latex because MathJax doesn't work in mail
+ (org-export-with-LaTeX-fragments
+ (if (executable-find "dvipng") 'dvipng
+ (mu4e-message "Cannot find dvipng, ignore inline LaTeX") nil))
+ ;; to hold attachments for inline html images
+ (html-and-images
+ (org~mu4e-mime-replace-images
+ (org-export-string-as raw-body 'html t)
+ tmp-file))
+ (html-images (cdr html-and-images))
+ (html (car html-and-images)))
+ (delete-region begin end)
+ (save-excursion
+ (goto-char begin)
+ (newline)
+ (insert (org~mu4e-mime-multipart
+ raw-body html (mapconcat 'identity html-images "\n"))))))
+
+;; next some functions to make the org/mu4e-compose-mode switch as smooth as
+;; possible.
+(defun org~mu4e-mime-decorate-headers ()
+ "Make the headers visually distinctive (org-mode)."
+ (save-excursion
+ (goto-char (point-min))
+ (let* ((eoh (when (search-forward mail-header-separator)
+ (match-end 0)))
+ (olay (make-overlay (point-min) eoh)))
+ (when olay
+ (overlay-put olay 'face 'font-lock-comment-face)))))
+
+(defun org~mu4e-mime-undecorate-headers ()
+ "Don't make the headers visually distinctive.
+\(well, mu4e-compose-mode will take care of that)."
+ (save-excursion
+ (goto-char (point-min))
+ (let* ((eoh (when (search-forward mail-header-separator)
+ (match-end 0))))
+ (remove-overlays (point-min) eoh))))
+
+(defvar org-mu4e-convert-to-html nil
+ "Whether to do automatic `org-mode' => html conversion when sending messages.")
+
+(defun org~mu4e-mime-convert-to-html-maybe ()
+ "Convert to html if `org-mu4e-convert-to-html' is non-nil.
+This function is called when sending a message (from
+`message-send-hook') and, if non-nil, sends the message as the
+rich-text version of what is assumed to be an org mode body."
+ (when org-mu4e-convert-to-html
+ (mu4e-message "Converting to html")
+ (org~mu4e-mime-convert-to-html)))
+
+(defun org~mu4e-mime-switch-headers-or-body ()
+ "Switch the buffer to either mu4e-compose-mode (when in headers)
+or org-mode (when in the body)."
+ (interactive)
+ (let* ((sepapoint
+ (save-excursion
+ (goto-char (point-min))
+ (search-forward-regexp mail-header-separator nil t))))
+ ;; only do stuff when the sepapoint exist; note that after sending the
+ ;; message, this function maybe called on a message with the sepapoint
+ ;; stripped. This is why we don't use `message-point-in-header'.
+ (when sepapoint
+ (cond
+ ;; we're in the body, but in mu4e-compose-mode?
+ ;; if so, switch to org-mode
+ ((and (> (point) sepapoint) (eq major-mode 'mu4e-compose-mode))
+ (org-mode)
+ (add-hook 'before-save-hook
+ #'org~mu4e-error-before-save-hook-fn
+ nil t)
+ (org~mu4e-mime-decorate-headers)
+ (local-set-key (kbd "M-m")
+ (lambda (keyseq)
+ (interactive "kEnter mu4e-compose-mode key sequence: ")
+ (let ((func (lookup-key mu4e-compose-mode-map keyseq)))
+ (if func (funcall func) (insert keyseq))))))
+ ;; we're in the headers, but in org-mode?
+ ;; if so, switch to mu4e-compose-mode
+ ((and (<= (point) sepapoint) (eq major-mode 'org-mode))
+ (org~mu4e-mime-undecorate-headers)
+ (mu4e-compose-mode)
+ (add-hook 'message-send-hook 'org~mu4e-mime-convert-to-html-maybe nil t)))
+ ;; and add the hook
+ (add-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t t))))
+
+(defun org~mu4e-error-before-save-hook-fn ()
+ (mu4e-error "Switch to mu4e-compose-mode (M-m) before saving"))
+
+(defun org-mu4e-compose-org-mode ()
+ "Defines a pseudo-minor mode for mu4e-compose-mode.
+Edit the message body using org mode. DEPRECATED."
+ (interactive)
+ (unless (member major-mode '(org-mode mu4e-compose-mode))
+ (mu4e-error "Need org-mode or mu4e-compose-mode"))
+ ;; we can check if we're already in org-mu4e-compose-mode by checking if the
+ ;; post-command-hook is set; hackish...but a buffer-local variable does not
+ ;; seem to survive buffer switching
+ (if (not (member 'org~mu4e-mime-switch-headers-or-body post-command-hook))
+ (progn
+ (org~mu4e-mime-switch-headers-or-body)
+ (mu4e-message
+ (concat
+ "org-mu4e-compose-org-mode enabled; "
+ "press M-m before issuing message-mode commands")))
+ (progn ;; otherwise, remove crap
+ (remove-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t)
+ (org~mu4e-mime-undecorate-headers) ;; shut off org-mode stuff
+ (mu4e-compose-mode)
+ (message "org-mu4e-compose-org-mode disabled"))))
+
+;;; _
+(provide 'org-mu4e)
+;;; org-mu4e.el ends here
--- /dev/null
+/*
+ Custom CSS for HTML documents generated with Texinfo's makeinfo.
+ Public domain 2016 sirgazil. All rights waived.
+*/
+
+
+
+/* NATIVE ELEMENTS */
+a:link,
+a:visited {
+ color: #245C8A;
+ text-decoration: none;
+}
+
+a:active,
+a:focus,
+a:hover {
+ text-decoration: underline;
+}
+
+abbr,
+acronym {
+ cursor: help;
+}
+
+blockquote {
+ color: #555753;
+ font-style: oblique;
+ margin: 30px 0px;
+ padding-left: 3em;
+}
+
+body {
+ background-color: white;
+ box-shadow: 0 0 2px gray;
+ box-sizing: border-box;
+ color: #333;
+ font-family: sans-serif;
+ font-size: 16px;
+ margin: 50px auto;
+ max-width: 960px;
+ padding: 50px;
+}
+
+code,
+samp,
+tt,
+var {
+ color: purple;
+ font-size: 0.8em;
+}
+
+div.example,
+div.lisp {
+ margin: 0px;
+}
+
+dl {
+ margin: 3em 0em;
+}
+
+dl dl {
+ margin: 0em;
+}
+
+dt {
+ background-color: #F5F5F5;
+ padding: 0.5em;
+}
+
+h1,
+h2,
+h2.contents-heading,
+h3,
+h4 {
+ padding: 20px 0px 0px 0px;
+ font-weight: normal;
+}
+
+h1 {
+ font-size: 2.4em;
+}
+
+h2 {
+ font-size: 2.2em;
+ font-weight: bold;
+}
+
+h3 {
+ font-size: 1.8em;
+}
+
+h4 {
+ font-size: 1.4em;
+}
+
+hr {
+ background-color: silver;
+ border-style: none;
+ height: 1px;
+ margin: 0px;
+}
+
+html {
+ background-color: #F5F5F5;
+}
+
+img {
+ max-width: 100%;
+}
+
+li {
+ padding: 5px;
+}
+
+pre.display,
+pre.example,
+pre.format,
+pre.lisp,
+pre.verbatim{
+ overflow: auto;
+}
+
+pre.example,
+pre.lisp,
+pre.verbatim {
+ background-color: #2D3743;
+ border-color: #000;
+ border-style: solid;
+ border-width: thin;
+ color: #E1E1E1;
+ font-size: smaller;
+ padding: 1em;
+}
+
+pre.menu-comment {
+ border-color: #E4E4E4;
+ border-bottom-style: solid;
+ border-width: thin;
+ font-family: sans;
+}
+
+table {
+ border-collapse: collapse;
+ margin: 40px 0px;
+}
+
+table.index-cp *,
+table.index-fn *,
+table.index-ky *,
+table.index-pg *,
+table.index-tp *,
+table.index-vr * {
+ background-color: inherit;
+ border-style: none;
+}
+
+td,
+th {
+ border-color: silver;
+ border-style: solid;
+ border-width: thin;
+ padding: 10px;
+}
+
+th {
+ background-color: #F5F5F5;
+}
+/* END NATIVE ELEMENTS */
+
+
+
+/* CLASSES */
+.contents {
+ margin-bottom: 4em;
+}
+
+.float {
+ margin: 3em 0em;
+}
+
+.float-caption {
+ font-size: smaller;
+ text-align: center;
+}
+
+.float > img {
+ display: block;
+ margin: auto;
+}
+
+.footnote {
+ font-size: smaller;
+ margin: 5em 0em;
+}
+
+.footnote h3 {
+ display: inline;
+ font-size: small;
+}
+
+.header {
+ background-color: #F2F2F2;
+ font-size: small;
+ padding: 0.2em 1em;
+}
+
+.key {
+ color: purple;
+ font-size: 0.8em;
+}
+
+.menu * {
+ border-style: none;
+}
+
+.menu td {
+ padding: 0.5em 0em;
+}
+
+.menu td:last-child {
+ width: 60%;
+}
+
+.menu th {
+ background-color: inherit;
+}
+/* END CLASSES */
--- /dev/null
+@set UPDATED @fulldate@
+@set UPDATED-MONTH @monthdate@
+@set EDITION @version@
+@set VERSION @version@
--- /dev/null
+@set UPDATED @UPDATED@
+@set UPDATED-MONTH @UPDATEDMONTH@
+@set EDITION @VERSION@
+@set VERSION @VERSION@