mu4e
This commit is contained in:
parent
d76764d98d
commit
a26811626d
|
@ -0,0 +1,66 @@
|
||||||
|
## 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-compose.el \
|
||||||
|
mu4e-context.el \
|
||||||
|
mu4e-contrib.el \
|
||||||
|
mu4e-draft.el \
|
||||||
|
mu4e-headers.el \
|
||||||
|
mu4e-icalendar.el \
|
||||||
|
mu4e-lists.el \
|
||||||
|
mu4e-main.el \
|
||||||
|
mu4e-mark.el \
|
||||||
|
mu4e-message.el \
|
||||||
|
mu4e-meta.el \
|
||||||
|
mu4e-org.el \
|
||||||
|
mu4e-proc.el \
|
||||||
|
mu4e-speedbar.el \
|
||||||
|
mu4e-utils.el \
|
||||||
|
mu4e-vars.el \
|
||||||
|
mu4e-view.el \
|
||||||
|
mu4e-view-common.el \
|
||||||
|
mu4e-view-gnus.el \
|
||||||
|
mu4e-view-old.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
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
||||||
|
* 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:
|
|
@ -0,0 +1,451 @@
|
||||||
|
@c The GNU Free Documentation License.
|
||||||
|
@center Version 1.2, November 2002
|
||||||
|
|
||||||
|
@c This file is intended to be included within another document,
|
||||||
|
@c hence no sectioning command or @node.
|
||||||
|
|
||||||
|
@display
|
||||||
|
Copyright @copyright{} 2000,2001,2002 Free Software Foundation, Inc.
|
||||||
|
51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
@end display
|
||||||
|
|
||||||
|
@enumerate 0
|
||||||
|
@item
|
||||||
|
PREAMBLE
|
||||||
|
|
||||||
|
The purpose of this License is to make a manual, textbook, or other
|
||||||
|
functional and useful document @dfn{free} in the sense of freedom: to
|
||||||
|
assure everyone the effective freedom to copy and redistribute it,
|
||||||
|
with or without modifying it, either commercially or noncommercially.
|
||||||
|
Secondarily, this License preserves for the author and publisher a way
|
||||||
|
to get credit for their work, while not being considered responsible
|
||||||
|
for modifications made by others.
|
||||||
|
|
||||||
|
This License is a kind of ``copyleft'', which means that derivative
|
||||||
|
works of the document must themselves be free in the same sense. It
|
||||||
|
complements the GNU General Public License, which is a copyleft
|
||||||
|
license designed for free software.
|
||||||
|
|
||||||
|
We have designed this License in order to use it for manuals for free
|
||||||
|
software, because free software needs free documentation: a free
|
||||||
|
program should come with manuals providing the same freedoms that the
|
||||||
|
software does. But this License is not limited to software manuals;
|
||||||
|
it can be used for any textual work, regardless of subject matter or
|
||||||
|
whether it is published as a printed book. We recommend this License
|
||||||
|
principally for works whose purpose is instruction or reference.
|
||||||
|
|
||||||
|
@item
|
||||||
|
APPLICABILITY AND DEFINITIONS
|
||||||
|
|
||||||
|
This License applies to any manual or other work, in any medium, that
|
||||||
|
contains a notice placed by the copyright holder saying it can be
|
||||||
|
distributed under the terms of this License. Such a notice grants a
|
||||||
|
world-wide, royalty-free license, unlimited in duration, to use that
|
||||||
|
work under the conditions stated herein. The ``Document'', below,
|
||||||
|
refers to any such manual or work. Any member of the public is a
|
||||||
|
licensee, and is addressed as ``you''. You accept the license if you
|
||||||
|
copy, modify or distribute the work in a way requiring permission
|
||||||
|
under copyright law.
|
||||||
|
|
||||||
|
A ``Modified Version'' of the Document means any work containing the
|
||||||
|
Document or a portion of it, either copied verbatim, or with
|
||||||
|
modifications and/or translated into another language.
|
||||||
|
|
||||||
|
A ``Secondary Section'' is a named appendix or a front-matter section
|
||||||
|
of the Document that deals exclusively with the relationship of the
|
||||||
|
publishers or authors of the Document to the Document's overall
|
||||||
|
subject (or to related matters) and contains nothing that could fall
|
||||||
|
directly within that overall subject. (Thus, if the Document is in
|
||||||
|
part a textbook of mathematics, a Secondary Section may not explain
|
||||||
|
any mathematics.) The relationship could be a matter of historical
|
||||||
|
connection with the subject or with related matters, or of legal,
|
||||||
|
commercial, philosophical, ethical or political position regarding
|
||||||
|
them.
|
||||||
|
|
||||||
|
The ``Invariant Sections'' are certain Secondary Sections whose titles
|
||||||
|
are designated, as being those of Invariant Sections, in the notice
|
||||||
|
that says that the Document is released under this License. If a
|
||||||
|
section does not fit the above definition of Secondary then it is not
|
||||||
|
allowed to be designated as Invariant. The Document may contain zero
|
||||||
|
Invariant Sections. If the Document does not identify any Invariant
|
||||||
|
Sections then there are none.
|
||||||
|
|
||||||
|
The ``Cover Texts'' are certain short passages of text that are listed,
|
||||||
|
as Front-Cover Texts or Back-Cover Texts, in the notice that says that
|
||||||
|
the Document is released under this License. A Front-Cover Text may
|
||||||
|
be at most 5 words, and a Back-Cover Text may be at most 25 words.
|
||||||
|
|
||||||
|
A ``Transparent'' copy of the Document means a machine-readable copy,
|
||||||
|
represented in a format whose specification is available to the
|
||||||
|
general public, that is suitable for revising the document
|
||||||
|
straightforwardly with generic text editors or (for images composed of
|
||||||
|
pixels) generic paint programs or (for drawings) some widely available
|
||||||
|
drawing editor, and that is suitable for input to text formatters or
|
||||||
|
for automatic translation to a variety of formats suitable for input
|
||||||
|
to text formatters. A copy made in an otherwise Transparent file
|
||||||
|
format whose markup, or absence of markup, has been arranged to thwart
|
||||||
|
or discourage subsequent modification by readers is not Transparent.
|
||||||
|
An image format is not Transparent if used for any substantial amount
|
||||||
|
of text. A copy that is not ``Transparent'' is called ``Opaque''.
|
||||||
|
|
||||||
|
Examples of suitable formats for Transparent copies include plain
|
||||||
|
@sc{ascii} without markup, Texinfo input format, La@TeX{} input
|
||||||
|
format, @acronym{SGML} or @acronym{XML} using a publicly available
|
||||||
|
@acronym{DTD}, and standard-conforming simple @acronym{HTML},
|
||||||
|
PostScript or @acronym{PDF} designed for human modification. Examples
|
||||||
|
of transparent image formats include @acronym{PNG}, @acronym{XCF} and
|
||||||
|
@acronym{JPG}. Opaque formats include proprietary formats that can be
|
||||||
|
read and edited only by proprietary word processors, @acronym{SGML} or
|
||||||
|
@acronym{XML} for which the @acronym{DTD} and/or processing tools are
|
||||||
|
not generally available, and the machine-generated @acronym{HTML},
|
||||||
|
PostScript or @acronym{PDF} produced by some word processors for
|
||||||
|
output purposes only.
|
||||||
|
|
||||||
|
The ``Title Page'' means, for a printed book, the title page itself,
|
||||||
|
plus such following pages as are needed to hold, legibly, the material
|
||||||
|
this License requires to appear in the title page. For works in
|
||||||
|
formats which do not have any title page as such, ``Title Page'' means
|
||||||
|
the text near the most prominent appearance of the work's title,
|
||||||
|
preceding the beginning of the body of the text.
|
||||||
|
|
||||||
|
A section ``Entitled XYZ'' means a named subunit of the Document whose
|
||||||
|
title either is precisely XYZ or contains XYZ in parentheses following
|
||||||
|
text that translates XYZ in another language. (Here XYZ stands for a
|
||||||
|
specific section name mentioned below, such as ``Acknowledgements'',
|
||||||
|
``Dedications'', ``Endorsements'', or ``History''.) To ``Preserve the Title''
|
||||||
|
of such a section when you modify the Document means that it remains a
|
||||||
|
section ``Entitled XYZ'' according to this definition.
|
||||||
|
|
||||||
|
The Document may include Warranty Disclaimers next to the notice which
|
||||||
|
states that this License applies to the Document. These Warranty
|
||||||
|
Disclaimers are considered to be included by reference in this
|
||||||
|
License, but only as regards disclaiming warranties: any other
|
||||||
|
implication that these Warranty Disclaimers may have is void and has
|
||||||
|
no effect on the meaning of this License.
|
||||||
|
|
||||||
|
@item
|
||||||
|
VERBATIM COPYING
|
||||||
|
|
||||||
|
You may copy and distribute the Document in any medium, either
|
||||||
|
commercially or noncommercially, provided that this License, the
|
||||||
|
copyright notices, and the license notice saying this License applies
|
||||||
|
to the Document are reproduced in all copies, and that you add no other
|
||||||
|
conditions whatsoever to those of this License. You may not use
|
||||||
|
technical measures to obstruct or control the reading or further
|
||||||
|
copying of the copies you make or distribute. However, you may accept
|
||||||
|
compensation in exchange for copies. If you distribute a large enough
|
||||||
|
number of copies you must also follow the conditions in section 3.
|
||||||
|
|
||||||
|
You may also lend copies, under the same conditions stated above, and
|
||||||
|
you may publicly display copies.
|
||||||
|
|
||||||
|
@item
|
||||||
|
COPYING IN QUANTITY
|
||||||
|
|
||||||
|
If you publish printed copies (or copies in media that commonly have
|
||||||
|
printed covers) of the Document, numbering more than 100, and the
|
||||||
|
Document's license notice requires Cover Texts, you must enclose the
|
||||||
|
copies in covers that carry, clearly and legibly, all these Cover
|
||||||
|
Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
|
||||||
|
the back cover. Both covers must also clearly and legibly identify
|
||||||
|
you as the publisher of these copies. The front cover must present
|
||||||
|
the full title with all words of the title equally prominent and
|
||||||
|
visible. You may add other material on the covers in addition.
|
||||||
|
Copying with changes limited to the covers, as long as they preserve
|
||||||
|
the title of the Document and satisfy these conditions, can be treated
|
||||||
|
as verbatim copying in other respects.
|
||||||
|
|
||||||
|
If the required texts for either cover are too voluminous to fit
|
||||||
|
legibly, you should put the first ones listed (as many as fit
|
||||||
|
reasonably) on the actual cover, and continue the rest onto adjacent
|
||||||
|
pages.
|
||||||
|
|
||||||
|
If you publish or distribute Opaque copies of the Document numbering
|
||||||
|
more than 100, you must either include a machine-readable Transparent
|
||||||
|
copy along with each Opaque copy, or state in or with each Opaque copy
|
||||||
|
a computer-network location from which the general network-using
|
||||||
|
public has access to download using public-standard network protocols
|
||||||
|
a complete Transparent copy of the Document, free of added material.
|
||||||
|
If you use the latter option, you must take reasonably prudent steps,
|
||||||
|
when you begin distribution of Opaque copies in quantity, to ensure
|
||||||
|
that this Transparent copy will remain thus accessible at the stated
|
||||||
|
location until at least one year after the last time you distribute an
|
||||||
|
Opaque copy (directly or through your agents or retailers) of that
|
||||||
|
edition to the public.
|
||||||
|
|
||||||
|
It is requested, but not required, that you contact the authors of the
|
||||||
|
Document well before redistributing any large number of copies, to give
|
||||||
|
them a chance to provide you with an updated version of the Document.
|
||||||
|
|
||||||
|
@item
|
||||||
|
MODIFICATIONS
|
||||||
|
|
||||||
|
You may copy and distribute a Modified Version of the Document under
|
||||||
|
the conditions of sections 2 and 3 above, provided that you release
|
||||||
|
the Modified Version under precisely this License, with the Modified
|
||||||
|
Version filling the role of the Document, thus licensing distribution
|
||||||
|
and modification of the Modified Version to whoever possesses a copy
|
||||||
|
of it. In addition, you must do these things in the Modified Version:
|
||||||
|
|
||||||
|
@enumerate A
|
||||||
|
@item
|
||||||
|
Use in the Title Page (and on the covers, if any) a title distinct
|
||||||
|
from that of the Document, and from those of previous versions
|
||||||
|
(which should, if there were any, be listed in the History section
|
||||||
|
of the Document). You may use the same title as a previous version
|
||||||
|
if the original publisher of that version gives permission.
|
||||||
|
|
||||||
|
@item
|
||||||
|
List on the Title Page, as authors, one or more persons or entities
|
||||||
|
responsible for authorship of the modifications in the Modified
|
||||||
|
Version, together with at least five of the principal authors of the
|
||||||
|
Document (all of its principal authors, if it has fewer than five),
|
||||||
|
unless they release you from this requirement.
|
||||||
|
|
||||||
|
@item
|
||||||
|
State on the Title page the name of the publisher of the
|
||||||
|
Modified Version, as the publisher.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Preserve all the copyright notices of the Document.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Add an appropriate copyright notice for your modifications
|
||||||
|
adjacent to the other copyright notices.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Include, immediately after the copyright notices, a license notice
|
||||||
|
giving the public permission to use the Modified Version under the
|
||||||
|
terms of this License, in the form shown in the Addendum below.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Preserve in that license notice the full lists of Invariant Sections
|
||||||
|
and required Cover Texts given in the Document's license notice.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Include an unaltered copy of this License.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Preserve the section Entitled ``History'', Preserve its Title, and add
|
||||||
|
to it an item stating at least the title, year, new authors, and
|
||||||
|
publisher of the Modified Version as given on the Title Page. If
|
||||||
|
there is no section Entitled ``History'' in the Document, create one
|
||||||
|
stating the title, year, authors, and publisher of the Document as
|
||||||
|
given on its Title Page, then add an item describing the Modified
|
||||||
|
Version as stated in the previous sentence.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Preserve the network location, if any, given in the Document for
|
||||||
|
public access to a Transparent copy of the Document, and likewise
|
||||||
|
the network locations given in the Document for previous versions
|
||||||
|
it was based on. These may be placed in the ``History'' section.
|
||||||
|
You may omit a network location for a work that was published at
|
||||||
|
least four years before the Document itself, or if the original
|
||||||
|
publisher of the version it refers to gives permission.
|
||||||
|
|
||||||
|
@item
|
||||||
|
For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve
|
||||||
|
the Title of the section, and preserve in the section all the
|
||||||
|
substance and tone of each of the contributor acknowledgements and/or
|
||||||
|
dedications given therein.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Preserve all the Invariant Sections of the Document,
|
||||||
|
unaltered in their text and in their titles. Section numbers
|
||||||
|
or the equivalent are not considered part of the section titles.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Delete any section Entitled ``Endorsements''. Such a section
|
||||||
|
may not be included in the Modified Version.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Do not retitle any existing section to be Entitled ``Endorsements'' or
|
||||||
|
to conflict in title with any Invariant Section.
|
||||||
|
|
||||||
|
@item
|
||||||
|
Preserve any Warranty Disclaimers.
|
||||||
|
@end enumerate
|
||||||
|
|
||||||
|
If the Modified Version includes new front-matter sections or
|
||||||
|
appendices that qualify as Secondary Sections and contain no material
|
||||||
|
copied from the Document, you may at your option designate some or all
|
||||||
|
of these sections as invariant. To do this, add their titles to the
|
||||||
|
list of Invariant Sections in the Modified Version's license notice.
|
||||||
|
These titles must be distinct from any other section titles.
|
||||||
|
|
||||||
|
You may add a section Entitled ``Endorsements'', provided it contains
|
||||||
|
nothing but endorsements of your Modified Version by various
|
||||||
|
parties---for example, statements of peer review or that the text has
|
||||||
|
been approved by an organization as the authoritative definition of a
|
||||||
|
standard.
|
||||||
|
|
||||||
|
You may add a passage of up to five words as a Front-Cover Text, and a
|
||||||
|
passage of up to 25 words as a Back-Cover Text, to the end of the list
|
||||||
|
of Cover Texts in the Modified Version. Only one passage of
|
||||||
|
Front-Cover Text and one of Back-Cover Text may be added by (or
|
||||||
|
through arrangements made by) any one entity. If the Document already
|
||||||
|
includes a cover text for the same cover, previously added by you or
|
||||||
|
by arrangement made by the same entity you are acting on behalf of,
|
||||||
|
you may not add another; but you may replace the old one, on explicit
|
||||||
|
permission from the previous publisher that added the old one.
|
||||||
|
|
||||||
|
The author(s) and publisher(s) of the Document do not by this License
|
||||||
|
give permission to use their names for publicity for or to assert or
|
||||||
|
imply endorsement of any Modified Version.
|
||||||
|
|
||||||
|
@item
|
||||||
|
COMBINING DOCUMENTS
|
||||||
|
|
||||||
|
You may combine the Document with other documents released under this
|
||||||
|
License, under the terms defined in section 4 above for modified
|
||||||
|
versions, provided that you include in the combination all of the
|
||||||
|
Invariant Sections of all of the original documents, unmodified, and
|
||||||
|
list them all as Invariant Sections of your combined work in its
|
||||||
|
license notice, and that you preserve all their Warranty Disclaimers.
|
||||||
|
|
||||||
|
The combined work need only contain one copy of this License, and
|
||||||
|
multiple identical Invariant Sections may be replaced with a single
|
||||||
|
copy. If there are multiple Invariant Sections with the same name but
|
||||||
|
different contents, make the title of each such section unique by
|
||||||
|
adding at the end of it, in parentheses, the name of the original
|
||||||
|
author or publisher of that section if known, or else a unique number.
|
||||||
|
Make the same adjustment to the section titles in the list of
|
||||||
|
Invariant Sections in the license notice of the combined work.
|
||||||
|
|
||||||
|
In the combination, you must combine any sections Entitled ``History''
|
||||||
|
in the various original documents, forming one section Entitled
|
||||||
|
``History''; likewise combine any sections Entitled ``Acknowledgements'',
|
||||||
|
and any sections Entitled ``Dedications''. You must delete all
|
||||||
|
sections Entitled ``Endorsements.''
|
||||||
|
|
||||||
|
@item
|
||||||
|
COLLECTIONS OF DOCUMENTS
|
||||||
|
|
||||||
|
You may make a collection consisting of the Document and other documents
|
||||||
|
released under this License, and replace the individual copies of this
|
||||||
|
License in the various documents with a single copy that is included in
|
||||||
|
the collection, provided that you follow the rules of this License for
|
||||||
|
verbatim copying of each of the documents in all other respects.
|
||||||
|
|
||||||
|
You may extract a single document from such a collection, and distribute
|
||||||
|
it individually under this License, provided you insert a copy of this
|
||||||
|
License into the extracted document, and follow this License in all
|
||||||
|
other respects regarding verbatim copying of that document.
|
||||||
|
|
||||||
|
@item
|
||||||
|
AGGREGATION WITH INDEPENDENT WORKS
|
||||||
|
|
||||||
|
A compilation of the Document or its derivatives with other separate
|
||||||
|
and independent documents or works, in or on a volume of a storage or
|
||||||
|
distribution medium, is called an ``aggregate'' if the copyright
|
||||||
|
resulting from the compilation is not used to limit the legal rights
|
||||||
|
of the compilation's users beyond what the individual works permit.
|
||||||
|
When the Document is included in an aggregate, this License does not
|
||||||
|
apply to the other works in the aggregate which are not themselves
|
||||||
|
derivative works of the Document.
|
||||||
|
|
||||||
|
If the Cover Text requirement of section 3 is applicable to these
|
||||||
|
copies of the Document, then if the Document is less than one half of
|
||||||
|
the entire aggregate, the Document's Cover Texts may be placed on
|
||||||
|
covers that bracket the Document within the aggregate, or the
|
||||||
|
electronic equivalent of covers if the Document is in electronic form.
|
||||||
|
Otherwise they must appear on printed covers that bracket the whole
|
||||||
|
aggregate.
|
||||||
|
|
||||||
|
@item
|
||||||
|
TRANSLATION
|
||||||
|
|
||||||
|
Translation is considered a kind of modification, so you may
|
||||||
|
distribute translations of the Document under the terms of section 4.
|
||||||
|
Replacing Invariant Sections with translations requires special
|
||||||
|
permission from their copyright holders, but you may include
|
||||||
|
translations of some or all Invariant Sections in addition to the
|
||||||
|
original versions of these Invariant Sections. You may include a
|
||||||
|
translation of this License, and all the license notices in the
|
||||||
|
Document, and any Warranty Disclaimers, provided that you also include
|
||||||
|
the original English version of this License and the original versions
|
||||||
|
of those notices and disclaimers. In case of a disagreement between
|
||||||
|
the translation and the original version of this License or a notice
|
||||||
|
or disclaimer, the original version will prevail.
|
||||||
|
|
||||||
|
If a section in the Document is Entitled ``Acknowledgements'',
|
||||||
|
``Dedications'', or ``History'', the requirement (section 4) to Preserve
|
||||||
|
its Title (section 1) will typically require changing the actual
|
||||||
|
title.
|
||||||
|
|
||||||
|
@item
|
||||||
|
TERMINATION
|
||||||
|
|
||||||
|
You may not copy, modify, sublicense, or distribute the Document except
|
||||||
|
as expressly provided for under this License. Any other attempt to
|
||||||
|
copy, modify, sublicense or distribute the Document is void, and will
|
||||||
|
automatically terminate your rights under this License. However,
|
||||||
|
parties who have received copies, or rights, from you under this
|
||||||
|
License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
@item
|
||||||
|
FUTURE REVISIONS OF THIS LICENSE
|
||||||
|
|
||||||
|
The Free Software Foundation may publish new, revised versions
|
||||||
|
of the GNU Free Documentation License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns. See
|
||||||
|
@uref{http://www.gnu.org/copyleft/}.
|
||||||
|
|
||||||
|
Each version of the License is given a distinguishing version number.
|
||||||
|
If the Document specifies that a particular numbered version of this
|
||||||
|
License ``or any later version'' applies to it, you have the option of
|
||||||
|
following the terms and conditions either of that specified version or
|
||||||
|
of any later version that has been published (not as a draft) by the
|
||||||
|
Free Software Foundation. If the Document does not specify a version
|
||||||
|
number of this License, you may choose any version ever published (not
|
||||||
|
as a draft) by the Free Software Foundation.
|
||||||
|
@end enumerate
|
||||||
|
|
||||||
|
@page
|
||||||
|
@heading ADDENDUM: How to use this License for your documents
|
||||||
|
|
||||||
|
To use this License in a document you have written, include a copy of
|
||||||
|
the License in the document and put the following copyright and
|
||||||
|
license notices just after the title page:
|
||||||
|
|
||||||
|
@smallexample
|
||||||
|
@group
|
||||||
|
Copyright (C) @var{year} @var{your name}.
|
||||||
|
Permission is granted to copy, distribute and/or modify this document
|
||||||
|
under the terms of the GNU Free Documentation License, Version 1.2
|
||||||
|
or any later version published by the Free Software Foundation;
|
||||||
|
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
|
||||||
|
Texts. A copy of the license is included in the section entitled ``GNU
|
||||||
|
Free Documentation License''.
|
||||||
|
@end group
|
||||||
|
@end smallexample
|
||||||
|
|
||||||
|
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
|
||||||
|
replace the ``with@dots{}Texts.'' line with this:
|
||||||
|
|
||||||
|
@smallexample
|
||||||
|
@group
|
||||||
|
with the Invariant Sections being @var{list their titles}, with
|
||||||
|
the Front-Cover Texts being @var{list}, and with the Back-Cover Texts
|
||||||
|
being @var{list}.
|
||||||
|
@end group
|
||||||
|
@end smallexample
|
||||||
|
|
||||||
|
If you have Invariant Sections without Cover Texts, or some other
|
||||||
|
combination of the three, merge those two alternatives to suit the
|
||||||
|
situation.
|
||||||
|
|
||||||
|
If your document contains nontrivial examples of program code, we
|
||||||
|
recommend releasing these examples in parallel under your choice of
|
||||||
|
free software license, such as the GNU General Public License,
|
||||||
|
to permit their use in free software.
|
||||||
|
|
||||||
|
@c Local Variables:
|
||||||
|
@c ispell-local-pdict: "ispell-dict"
|
||||||
|
@c End:
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
#+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]
|
|
@ -0,0 +1,253 @@
|
||||||
|
;;; mu4e-actions.el -- part of mu4e, the mu mail user agent -*- 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:
|
||||||
|
|
||||||
|
;; Example actions for messages, attachments (see chapter 'Actions' in the
|
||||||
|
;; manual)
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'ido)
|
||||||
|
|
||||||
|
(require 'mu4e-utils)
|
||||||
|
(require 'mu4e-message)
|
||||||
|
(require 'mu4e-meta)
|
||||||
|
|
||||||
|
(declare-function mu4e~proc-extract "mu4e-proc")
|
||||||
|
(declare-function mu4e-headers-search "mu4e-headers")
|
||||||
|
|
||||||
|
(defvar mu4e-headers-include-related)
|
||||||
|
(defvar mu4e-headers-show-threads)
|
||||||
|
(defvar mu4e-view-show-addresses)
|
||||||
|
(defvar mu4e-view-date-format)
|
||||||
|
|
||||||
|
|
||||||
|
;;; 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 (car-safe sender)) (email (cdr-safe 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)))))
|
||||||
|
(message "%S" org-capture-templates)
|
||||||
|
(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)))))
|
||||||
|
|
||||||
|
(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-headers-show-threads t)
|
||||||
|
(mu4e-headers-include-related t))
|
||||||
|
(mu4e-headers-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
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,183 @@
|
||||||
|
;;; mu4e-context.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2015-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:
|
||||||
|
|
||||||
|
;; A mu4e 'context' is a set of variable-settings and functions, which can be
|
||||||
|
;; used e.g. to switch between accounts.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'mu4e-utils)
|
||||||
|
|
||||||
|
(defvar smtpmail-smtp-user)
|
||||||
|
(defvar mu4e-view-date-format)
|
||||||
|
|
||||||
|
(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.")
|
||||||
|
|
||||||
|
(defvar mu4e~context-current nil
|
||||||
|
"The current context; for internal use. Use
|
||||||
|
`mu4e-context-switch' to change it.")
|
||||||
|
|
||||||
|
(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, or \"\" 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."
|
||||||
|
(when mu4e-contexts
|
||||||
|
(let* ((names (cl-map 'list (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 context to a context with NAME which is 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 (cl-map 'list (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)
|
||||||
|
"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 with a match-func that returns t. 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?
|
||||||
|
(cl-find-if (lambda (context)
|
||||||
|
(when (mu4e-context-match-func context)
|
||||||
|
(funcall (mu4e-context-match-func context) msg)))
|
||||||
|
mu4e-contexts)
|
||||||
|
;; no context found yet; consult policy
|
||||||
|
(cl-case 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: ")))
|
||||||
|
(otherwise 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))))
|
||||||
|
|
||||||
|
;;; _
|
||||||
|
(provide 'mu4e-context)
|
||||||
|
;;; mu4e-context.el ends here
|
|
@ -0,0 +1,224 @@
|
||||||
|
;;; mu4e-contrib.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2013-2021 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)
|
||||||
|
|
||||||
|
;; Contributed by sabof
|
||||||
|
(defvar bookmark-make-record-function)
|
||||||
|
|
||||||
|
;;; 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 messages within current query results and ask user to execute which action."
|
||||||
|
(interactive)
|
||||||
|
(mu4e-headers-mark-for-each-if
|
||||||
|
(cons 'something nil)
|
||||||
|
(lambda (_msg _param) t))
|
||||||
|
(mu4e-mark-execute-all))
|
||||||
|
|
||||||
|
;;; Bookmark handlers
|
||||||
|
;;
|
||||||
|
;; Allow bookmarking a mu4e buffer in regular emacs bookmarks.
|
||||||
|
|
||||||
|
(defun mu4e~view-set-bookmark-make-record-fn ()
|
||||||
|
(set (make-local-variable 'bookmark-make-record-function)
|
||||||
|
'mu4e-view-bookmark-make-record))
|
||||||
|
|
||||||
|
(defun mu4e~headers-set-bookmark-make-record-fn ()
|
||||||
|
(set (make-local-variable 'bookmark-make-record-function)
|
||||||
|
'mu4e-view-bookmark-make-record))
|
||||||
|
|
||||||
|
;; Probably this can be moved to mu4e-view.el.
|
||||||
|
(add-hook 'mu4e-view-mode-hook #'mu4e~view-set-bookmark-make-record-fn)
|
||||||
|
;; And this can be moved to mu4e-headers.el.
|
||||||
|
(add-hook 'mu4e-headers-mode-hook #'mu4e~headers-set-bookmark-make-record-fn)
|
||||||
|
|
||||||
|
(defun mu4e-view-bookmark-make-record ()
|
||||||
|
"Make a bookmark entry for a mu4e buffer. Note that this is an
|
||||||
|
emacs bookmark, not to be confused with `mu4e-bookmarks'."
|
||||||
|
(let* ((msg (mu4e-message-at-point))
|
||||||
|
(maildir (plist-get msg :maildir))
|
||||||
|
(date (format-time-string "%Y%m%d" (plist-get msg :date)))
|
||||||
|
(query (format "maildir:%s date:%s" maildir date))
|
||||||
|
(docid (plist-get msg :docid))
|
||||||
|
(mode (symbol-name major-mode))
|
||||||
|
(subject (or (plist-get msg :subject) "No subject")))
|
||||||
|
`(,subject
|
||||||
|
,@(bookmark-make-record-default 'no-file 'no-context)
|
||||||
|
(location . (,query . ,docid))
|
||||||
|
(mode . ,mode)
|
||||||
|
(handler . mu4e-bookmark-jump))))
|
||||||
|
|
||||||
|
(defun mu4e-bookmark-jump (bookmark)
|
||||||
|
"Handler function for record returned by `mu4e-view-bookmark-make-record'.
|
||||||
|
BOOKMARK is a bookmark name or a bookmark record."
|
||||||
|
(let* ((path (bookmark-prop-get bookmark 'location))
|
||||||
|
(mode (bookmark-prop-get bookmark 'mode))
|
||||||
|
(docid (cdr path))
|
||||||
|
(query (car path)))
|
||||||
|
(call-interactively 'mu4e)
|
||||||
|
(mu4e-headers-search query)
|
||||||
|
(sit-for 0.5)
|
||||||
|
(mu4e~headers-goto-docid docid)
|
||||||
|
(mu4e~headers-highlight docid)
|
||||||
|
(unless (string= mode "mu4e-headers-mode")
|
||||||
|
(call-interactively 'mu4e-headers-view-message)
|
||||||
|
(run-with-timer 0.1 nil
|
||||||
|
(lambda (bmk)
|
||||||
|
(bookmark-default-handler
|
||||||
|
`("" (buffer . ,(current-buffer)) .
|
||||||
|
,(bookmark-get-bookmark-record bmk))))
|
||||||
|
bookmark))))
|
||||||
|
|
||||||
|
;;; 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)
|
||||||
|
"Mark message as spam."
|
||||||
|
(interactive)
|
||||||
|
(let* ((path (shell-quote-argument (mu4e-message-field msg :path)))
|
||||||
|
(command (format mu4e-register-as-spam-cmd path))) ;; re-register msg as spam
|
||||||
|
(shell-command command))
|
||||||
|
(mu4e-mark-at-point 'delete nil))
|
||||||
|
|
||||||
|
(defun mu4e-register-msg-as-ham (msg)
|
||||||
|
"Mark message as ham."
|
||||||
|
(interactive)
|
||||||
|
(let* ((path (shell-quote-argument(mu4e-message-field msg :path)))
|
||||||
|
(command (format mu4e-register-as-ham-cmd path))) ;; re-register msg as ham
|
||||||
|
(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)
|
||||||
|
"Mark message 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 message 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))
|
||||||
|
|
||||||
|
;;; 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 eshell/mu4e-attach (&rest args)
|
||||||
|
"Attach files to a mu4e message using eshell. 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
|
|
@ -0,0 +1,604 @@
|
||||||
|
;;; mu4e-draft.el -- part of mu4e, the mu mail user agent for emacs -*- lexical-binding: t -*-
|
||||||
|
;;
|
||||||
|
;; Copyright (C) 2011-2020 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-vars)
|
||||||
|
(require 'mu4e-utils)
|
||||||
|
(require 'mu4e-message)
|
||||||
|
(require 'message) ;; mail-header-separator
|
||||||
|
|
||||||
|
;;; Options
|
||||||
|
|
||||||
|
(defcustom mu4e-compose-dont-reply-to-self nil
|
||||||
|
"If non-nil, don't include self.
|
||||||
|
(as 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)
|
||||||
|
|
||||||
|
(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~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 " ")))
|
||||||
|
|
||||||
|
|
||||||
|
;;; 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 (addrcell)
|
||||||
|
(let ((name (car addrcell))
|
||||||
|
(email (cdr addrcell)))
|
||||||
|
(if name
|
||||||
|
(format "%s <%s>" (mu4e~rfc822-quoteit name) email)
|
||||||
|
(format "%s" email))))
|
||||||
|
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 (cdr cell1) ""))
|
||||||
|
(downcase (or (cdr 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 (cdr 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 (cdr 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 (cdr 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 (cdr 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")))))
|
||||||
|
|
||||||
|
;;; 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 'rfc822-quoted-string). Then whether there is a quote
|
||||||
|
inside the phrase (returning 'rfc822-containing-quote).
|
||||||
|
The reverse of the RFC atext definition is then tested.
|
||||||
|
If it matches, nil is returned, if not, it is an 'rfc822-atom, which
|
||||||
|
is returned."
|
||||||
|
(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-quoteit (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)))))
|
||||||
|
|
||||||
|
|
||||||
|
(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
|
||||||
|
(if user-full-name
|
||||||
|
(format "%s <%s>" (mu4e~rfc822-quoteit user-full-name) user-mail-address)
|
||||||
|
(format "%s" 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 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))))
|
||||||
|
(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)" (cdar from)) . 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
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,211 @@
|
||||||
|
;;; 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-utils)
|
||||||
|
(require 'mu4e-headers)
|
||||||
|
(require 'mu4e-view)
|
||||||
|
(require 'mu4e-vars)
|
||||||
|
|
||||||
|
(when mu4e-view-use-old
|
||||||
|
(mu4e-error "iCalender support is not available with the old viewer"))
|
||||||
|
|
||||||
|
;;;###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 (cdr 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 the organiser.
|
||||||
|
(let* ((organizer (gnus-icalendar-event:organizer event))
|
||||||
|
(reply-to (plist-get msg :reply-to))
|
||||||
|
(name (or (caar reply-to)
|
||||||
|
(caar (plist-get msg :from))))
|
||||||
|
(email (cons name organizer)))
|
||||||
|
(unless (or (string= organizer "")
|
||||||
|
(mu4e~icalendar-has-email organizer reply-to))
|
||||||
|
(plist-put ical-msg :reply-to (cons email reply-to))))
|
||||||
|
(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
|
|
@ -0,0 +1,102 @@
|
||||||
|
;;; mu4e-lists.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2011-2016 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:
|
||||||
|
|
||||||
|
(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 regex patterns 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)))
|
||||||
|
|
||||||
|
;;; _
|
||||||
|
(provide 'mu4e-lists)
|
||||||
|
;;; mu4e-lists.el ends here
|
|
@ -0,0 +1,387 @@
|
||||||
|
;;; mu4e-main.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2011-2020 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-utils) ;; utility functions
|
||||||
|
(require 'mu4e-context) ;; the context
|
||||||
|
(require 'mu4e-vars) ;; mu-wide variables
|
||||||
|
(require 'cl-lib)
|
||||||
|
|
||||||
|
;;; Mode
|
||||||
|
|
||||||
|
(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.
|
||||||
|
|
||||||
|
This also hides the warning if your `user-mail-address' is not
|
||||||
|
part of the personal addresses.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defvar mu4e-main-hide-fully-read nil
|
||||||
|
"When set to t, do not hide bookmarks or maildirs that have
|
||||||
|
no unread messages.")
|
||||||
|
|
||||||
|
(defvar mu4e-main-mode-map
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
|
||||||
|
(define-key map "b" 'mu4e-headers-search-bookmark)
|
||||||
|
(define-key map "B" 'mu4e-headers-search-bookmark-edit)
|
||||||
|
|
||||||
|
(define-key map "s" 'mu4e-headers-search)
|
||||||
|
(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.")
|
||||||
|
|
||||||
|
(defvar mu4e-main-mode-abbrev-table nil)
|
||||||
|
(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-in-modeline)
|
||||||
|
(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~main-bookmarks ()
|
||||||
|
;; 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)
|
||||||
|
"Return a key / value pair."
|
||||||
|
(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))
|
||||||
|
|
||||||
|
(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 ()
|
||||||
|
(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-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 "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 ()
|
||||||
|
"mu4e main view in the minibuffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((key
|
||||||
|
(read-key
|
||||||
|
(mu4e-format
|
||||||
|
"%s"
|
||||||
|
(concat
|
||||||
|
(mu4e~main-action-str "[j]ump " 'mu4e-jump-to-maildir)
|
||||||
|
(mu4e~main-action-str "[s]earch " 'mu4e-search)
|
||||||
|
(mu4e~main-action-str "[C]ompose " 'mu4e-compose-new)
|
||||||
|
(mu4e~main-action-str "[b]ookmarks " 'mu4e-headers-search-bookmark)
|
||||||
|
(mu4e~main-action-str "[;]Switch context " 'mu4e-context-switch)
|
||||||
|
(mu4e~main-action-str "[U]pdate " 'mu4e-update-mail-and-index)
|
||||||
|
(mu4e~main-action-str "[N]ews " 'mu4e-news)
|
||||||
|
(mu4e~main-action-str "[A]bout " 'mu4e-about)
|
||||||
|
(mu4e~main-action-str "[H]elp " 'mu4e-display-manual))))))
|
||||||
|
(unless (member key '(?\C-g ?\C-\[))
|
||||||
|
(let ((mu4e-command (lookup-key mu4e-main-mode-map (string key) t)))
|
||||||
|
(if mu4e-command
|
||||||
|
(condition-case err
|
||||||
|
(let ((mu4e-hide-index-messages t))
|
||||||
|
(call-interactively mu4e-command))
|
||||||
|
(error (when (cadr err) (message (cadr err)))))
|
||||||
|
(message (mu4e-format "key %s not bound to a command" (string key))))
|
||||||
|
(when (or (not mu4e-command) (eq mu4e-command 'mu4e-context-switch))
|
||||||
|
(sit-for 1)
|
||||||
|
(mu4e~main-menu))))))
|
||||||
|
|
||||||
|
;;; _
|
||||||
|
(provide 'mu4e-main)
|
||||||
|
;;; mu4e-main.el ends here
|
|
@ -0,0 +1,470 @@
|
||||||
|
;;; mu4e-mark.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2011-2020 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 'cl-lib)
|
||||||
|
(require 'mu4e-proc)
|
||||||
|
(require 'mu4e-utils)
|
||||||
|
(require 'mu4e-message)
|
||||||
|
|
||||||
|
;; 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 an Emacs feature called 'overlays', which aren't
|
||||||
|
particularly fast).")
|
||||||
|
|
||||||
|
;;; 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."
|
||||||
|
(cl-find-if
|
||||||
|
(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~proc-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~proc-remove docid)))
|
||||||
|
(flag
|
||||||
|
:char ("+" . "✚")
|
||||||
|
:prompt "+flag"
|
||||||
|
:show-target (lambda (target) "flag")
|
||||||
|
:action (lambda (docid msg target)
|
||||||
|
(mu4e~proc-move docid nil "+F-u-N")))
|
||||||
|
(move
|
||||||
|
:char ("m" . "▷")
|
||||||
|
:prompt "move"
|
||||||
|
:ask-target mu4e~mark-get-move-target
|
||||||
|
:action (lambda (docid msg target)
|
||||||
|
(mu4e~proc-move docid (mu4e~mark-check-target target) "-N")))
|
||||||
|
(read
|
||||||
|
:char ("!" . "◼")
|
||||||
|
:prompt "!read"
|
||||||
|
:show-target (lambda (target) "read")
|
||||||
|
:action (lambda (docid msg target) (mu4e~proc-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~proc-move docid
|
||||||
|
(mu4e~mark-check-target target) "+T-N")))
|
||||||
|
(unflag
|
||||||
|
:char ("-" . "➖")
|
||||||
|
:prompt "-unflag"
|
||||||
|
:show-target (lambda (target) "unflag")
|
||||||
|
:action (lambda (docid msg target) (mu4e~proc-move docid nil "-F-N")))
|
||||||
|
(untrash
|
||||||
|
:char ("=" . "▲")
|
||||||
|
:prompt "=untrash"
|
||||||
|
:show-target (lambda (target) "untrash")
|
||||||
|
:action (lambda (docid msg target) (mu4e~proc-move docid nil "-T")))
|
||||||
|
(unread
|
||||||
|
:char ("?" . "◻")
|
||||||
|
:prompt "?unread"
|
||||||
|
:show-target (lambda (target) "unread")
|
||||||
|
:action (lambda (docid msg target) (mu4e~proc-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 'target' '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~proc-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 (cl-remove-if (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
|
|
@ -0,0 +1,364 @@
|
||||||
|
;;; mu4e-message.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2012-2020 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 'cl-lib)
|
||||||
|
(require 'mu4e-vars)
|
||||||
|
(require 'flow-fill)
|
||||||
|
(require 'shr)
|
||||||
|
|
||||||
|
(declare-function mu4e-error "mu4e-utils")
|
||||||
|
(declare-function mu4e-warn "mu4e-utils")
|
||||||
|
(declare-function mu4e-personal-address-p "mu4e-utils")
|
||||||
|
(declare-function mu4e-make-temp-file "mu4e-utils")
|
||||||
|
|
||||||
|
(defvar mu4e~view-message)
|
||||||
|
(defvar shr-inhibit-images)
|
||||||
|
|
||||||
|
(defcustom mu4e-html2text-command 'mu4e-shr2text
|
||||||
|
"Either a shell command or a function that converts from html to plain text.
|
||||||
|
|
||||||
|
If it is a shell command, the command reads html from standard
|
||||||
|
input and outputs plain text on standard output. If you use the
|
||||||
|
htmltext program, it's recommended you use \"html2text -utf8
|
||||||
|
-width 72\". Alternatives are the python-based html2markdown, w3m
|
||||||
|
and on MacOS you may want to use textutil.
|
||||||
|
|
||||||
|
It can also be a function, which takes a messsage-plist as
|
||||||
|
argument and is expected to return the textified html as output.
|
||||||
|
|
||||||
|
For backward compatibility, it can also be a parameterless
|
||||||
|
function which is run in the context of a buffer with the html
|
||||||
|
and expected to transform this (like the `html2text' function).
|
||||||
|
|
||||||
|
In all cases, the output is expected to be in UTF-8 encoding.
|
||||||
|
|
||||||
|
The default is to use the shr renderer."
|
||||||
|
:type '(choice string function)
|
||||||
|
:group 'mu4e-view)
|
||||||
|
|
||||||
|
(defcustom mu4e-view-prefer-html nil
|
||||||
|
"Whether to base the body display on the html-version.
|
||||||
|
If the e-mail message has no html-version the plain-text version
|
||||||
|
is always used."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'mu4e-view)
|
||||||
|
|
||||||
|
(defcustom mu4e-view-html-plaintext-ratio-heuristic 5
|
||||||
|
"Ratio between the length of the html and the plain text part.
|
||||||
|
Below this ratio mu4e will consider the plain text part to be
|
||||||
|
'This messages requires html' text bodies. You can neutralize
|
||||||
|
it (always show the text version) by using
|
||||||
|
`most-positive-fixnum'."
|
||||||
|
:type 'integer
|
||||||
|
:group 'mu4e-view)
|
||||||
|
|
||||||
|
(defvar mu4e-message-body-rewrite-functions '(mu4e-message-outlook-cleanup)
|
||||||
|
"List of functions to transform the message body text.
|
||||||
|
The functions take two parameters, MSG and TXT, which are the
|
||||||
|
message-plist and the text, which is the plain-text version,
|
||||||
|
ossibly converted from html and/or transformed by earlier rewrite
|
||||||
|
functions.")
|
||||||
|
|
||||||
|
;;; Message fields
|
||||||
|
|
||||||
|
(defsubst mu4e-message-field-raw (msg field)
|
||||||
|
"Retrieve FIELD from message plist MSG.
|
||||||
|
FIELD is one of :from, :to, :cc, :bcc, :subject, :data,
|
||||||
|
:message-id, :path, :maildir, :priority, :attachments,
|
||||||
|
:references, :in-reply-to, :body-txt, :body-html
|
||||||
|
|
||||||
|
Returns nil if the field does not exist.
|
||||||
|
|
||||||
|
A message plist looks something like:
|
||||||
|
\(:docid 32461
|
||||||
|
:from ((\"Nikola Tesla\" . \"niko@example.com\"))
|
||||||
|
:to ((\"Thomas Edison\" . \"tom@example.com\"))
|
||||||
|
:cc ((\"Rupert The Monkey\" . \"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)
|
||||||
|
:attachments
|
||||||
|
((:index 2 :name \"photo.jpg\" :mime-type \"image/jpeg\" :size 147331)
|
||||||
|
(:index 3 :name \"book.pdf\" :mime-type \"application/pdf\" :size 192220))
|
||||||
|
:references (\"238C8384574032D81EE81AF0114E4E74@123213.mail.example.com\"
|
||||||
|
\"6BDC23465F79238203498230942D81EE81AF0114E4E74@123213.mail.example.com\")
|
||||||
|
:in-reply-to \"238203498230942D81EE81AF0114E4E74@123213.mail.example.com\"
|
||||||
|
:body-txt \"Hi Tom, ...\"
|
||||||
|
\)).
|
||||||
|
Some notes on the format:
|
||||||
|
- The address fields are lists of pairs (NAME . EMAIL), where NAME can be nil.
|
||||||
|
- 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."
|
||||||
|
(let ((msg (or (get-text-property (point) 'msg) mu4e~view-message)))
|
||||||
|
(if msg
|
||||||
|
msg
|
||||||
|
(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))
|
||||||
|
|
||||||
|
(defvar mu4e~message-body-html nil
|
||||||
|
"Whether the body text uses HTML.")
|
||||||
|
|
||||||
|
(defun mu4e~message-use-html-p (msg prefer-html)
|
||||||
|
"Do we want to PREFER-HTML for MSG?
|
||||||
|
Determine whether we want
|
||||||
|
to use html or text. The decision is based on PREFER-HTML and
|
||||||
|
whether the message supports the given representation."
|
||||||
|
(let* ((txt (mu4e-message-field msg :body-txt))
|
||||||
|
(html (mu4e-message-field msg :body-html))
|
||||||
|
(txt-len (length txt))
|
||||||
|
(html-len (length html))
|
||||||
|
(txt-limit (* mu4e-view-html-plaintext-ratio-heuristic txt-len))
|
||||||
|
(txt-limit (if (>= txt-limit 0) txt-limit most-positive-fixnum)))
|
||||||
|
(cond
|
||||||
|
; user prefers html --> use html if there is
|
||||||
|
(prefer-html (> html-len 0))
|
||||||
|
;; otherwise (user prefers text) still use html if there is not enough
|
||||||
|
;; text
|
||||||
|
((< txt-limit html-len) t)
|
||||||
|
;; otherwise, use text
|
||||||
|
(t nil))))
|
||||||
|
|
||||||
|
(defun mu4e~message-body-has-content-type-param (msg param)
|
||||||
|
"Does the MSG have a content-type parameter PARAM?"
|
||||||
|
(cdr
|
||||||
|
(assoc param (mu4e-message-field msg :body-txt-params))))
|
||||||
|
|
||||||
|
(defun mu4e~safe-iequal (a b)
|
||||||
|
"Is string A equal to a downcased B?"
|
||||||
|
(and b (equal (downcase b) a)))
|
||||||
|
|
||||||
|
(defun mu4e-message-body-text (msg &optional prefer-html)
|
||||||
|
"Get the body in text form for message MSG.
|
||||||
|
This is either :body-txt, or if not available, :body-html
|
||||||
|
converted to text, using `mu4e-html2text-command' is non-nil, it
|
||||||
|
will use that. Normally, this function prefers the text part,
|
||||||
|
unless PREFER-HTML is non-nil."
|
||||||
|
(setq mu4e~message-body-html (mu4e~message-use-html-p msg prefer-html))
|
||||||
|
(let ((body
|
||||||
|
(if mu4e~message-body-html
|
||||||
|
;; use an htmml body
|
||||||
|
(cond
|
||||||
|
((stringp mu4e-html2text-command)
|
||||||
|
(mu4e~html2text-shell msg mu4e-html2text-command))
|
||||||
|
((functionp mu4e-html2text-command)
|
||||||
|
(if (help-function-arglist mu4e-html2text-command)
|
||||||
|
(funcall mu4e-html2text-command msg)
|
||||||
|
;; oldskool parameterless mu4e-html2text-command
|
||||||
|
(mu4e~html2text-wrapper mu4e-html2text-command msg)))
|
||||||
|
(t (mu4e-error "Invalid `mu4e-html2text-command'")))
|
||||||
|
;; use a text body
|
||||||
|
(or (with-temp-buffer
|
||||||
|
(insert (or (mu4e-message-field msg :body-txt) ""))
|
||||||
|
(if (mu4e~safe-iequal "flowed"
|
||||||
|
(mu4e~message-body-has-content-type-param
|
||||||
|
msg "format"))
|
||||||
|
(fill-flowed nil
|
||||||
|
(mu4e~safe-iequal
|
||||||
|
"yes"
|
||||||
|
(mu4e~message-body-has-content-type-param
|
||||||
|
msg "delsp"))))
|
||||||
|
(buffer-string)) ""))))
|
||||||
|
(dolist (func mu4e-message-body-rewrite-functions)
|
||||||
|
(setq body (funcall func msg body)))
|
||||||
|
body))
|
||||||
|
|
||||||
|
(defun mu4e-message-outlook-cleanup (_msg body)
|
||||||
|
"Clean-up MSG's BODY.
|
||||||
|
Esp. MS-Outlook-originating message may not advertise the correct
|
||||||
|
encoding (e.g. 'iso-8859-1' instead of 'windows-1252'), thus
|
||||||
|
giving us these funky chars. here, we either remove them, or
|
||||||
|
replace with."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert body)
|
||||||
|
(goto-char (point-min))
|
||||||
|
(while (re-search-forward "[
|
||||||
|
]" nil t)
|
||||||
|
(replace-match
|
||||||
|
(cond
|
||||||
|
((string= (match-string 0) "") "'")
|
||||||
|
((string= (match-string 0) " ") " ")
|
||||||
|
(t ""))))
|
||||||
|
(buffer-string)))
|
||||||
|
|
||||||
|
(defun mu4e-message-contact-field-matches (msg cfield rx)
|
||||||
|
"Does MSG's contact-field CFIELD match 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."
|
||||||
|
(if (and cfield (listp cfield))
|
||||||
|
(or (mu4e-message-contact-field-matches msg (car cfield) rx)
|
||||||
|
(mu4e-message-contact-field-matches msg (cdr cfield) rx))
|
||||||
|
(when cfield
|
||||||
|
(if (listp rx)
|
||||||
|
;; if rx is a list, try each one of them for a match
|
||||||
|
(cl-find-if
|
||||||
|
(lambda (a-rx) (mu4e-message-contact-field-matches msg cfield a-rx))
|
||||||
|
rx)
|
||||||
|
;; not a list, check the rx
|
||||||
|
(cl-find-if
|
||||||
|
(lambda (ct)
|
||||||
|
(let ((name (car ct)) (email (cdr 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."
|
||||||
|
(cl-find-if (lambda (cell) (mu4e-personal-address-p (cdr cell)))
|
||||||
|
(mu4e-message-field msg cfield)))
|
||||||
|
|
||||||
|
(defun mu4e-message-sent-by-me (msg)
|
||||||
|
"Is this message (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 message have user's personal address in any of the
|
||||||
|
contact fields?"
|
||||||
|
(cl-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)
|
||||||
|
(defalias 'mu4e-body-text 'mu4e-message-body-text) ;; backward compatibility
|
||||||
|
|
||||||
|
(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
|
||||||
|
|
||||||
|
(defun mu4e~html2text-wrapper (func msg)
|
||||||
|
"Apply FUNC on a temporary buffer with html from MSG.
|
||||||
|
Return the buffer contents."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert (or (mu4e-message-field msg :body-html) ""))
|
||||||
|
(funcall func)
|
||||||
|
(or (buffer-string) "")))
|
||||||
|
|
||||||
|
(defun mu4e-shr2text (msg)
|
||||||
|
"Convert html in MSG to text using the shr engine.
|
||||||
|
This can be used in `mu4e-html2text-command' in a new enough
|
||||||
|
Emacs. Based on code by Titus von der Malsburg."
|
||||||
|
(mu4e~html2text-wrapper
|
||||||
|
(lambda ()
|
||||||
|
(let (
|
||||||
|
;; When HTML emails contain references to remote images,
|
||||||
|
;; retrieving these images leaks information. For example,
|
||||||
|
;; the sender can see when I opened the email and from which
|
||||||
|
;; computer (IP address). For this reason, it is preferable
|
||||||
|
;; to not retrieve images.
|
||||||
|
;; See this discussion on mu-discuss:
|
||||||
|
;; https://groups.google.com/forum/#!topic/mu-discuss/gr1cwNNZnXo
|
||||||
|
(shr-inhibit-images t))
|
||||||
|
(shr-render-region (point-min) (point-max)))) msg))
|
||||||
|
|
||||||
|
(defun mu4e~html2text-shell (msg _cmd)
|
||||||
|
"Convert html2 text in MSG using a shell function CMD."
|
||||||
|
(mu4e~html2text-wrapper
|
||||||
|
(lambda ()
|
||||||
|
(let* ((tmp-file (mu4e-make-temp-file "html")))
|
||||||
|
(write-region (point-min) (point-max) tmp-file)
|
||||||
|
(erase-buffer)
|
||||||
|
(call-process-shell-command mu4e-html2text-command tmp-file t t)
|
||||||
|
(delete-file tmp-file))) msg))
|
||||||
|
|
||||||
|
;;; _
|
||||||
|
(provide 'mu4e-message)
|
||||||
|
;;; mu4e-message.el ends here
|
|
@ -0,0 +1,11 @@
|
||||||
|
;; auto-generated
|
||||||
|
(defconst mu4e-mu-version "1.6.1"
|
||||||
|
"Required mu binary version; mu4e's version must agree with this.")
|
||||||
|
|
||||||
|
(defconst mu4e-builddir "/home/djcb/Sources/mu"
|
||||||
|
"Top-level build directory.")
|
||||||
|
|
||||||
|
(defconst mu4e-doc-dir "/usr/local/share/doc/mu"
|
||||||
|
"Mu4e's data-dir.")
|
||||||
|
|
||||||
|
(provide 'mu4e-meta)
|
|
@ -0,0 +1,11 @@
|
||||||
|
;; 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-meta)
|
|
@ -0,0 +1,127 @@
|
||||||
|
;;; mu4e-org -- Org-links to mu4e messages/queries -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2012-2021 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-utils)
|
||||||
|
|
||||||
|
(defgroup mu4e-org nil
|
||||||
|
"Settings for the org-mode related functionality in mu4e."
|
||||||
|
:group 'mu4e
|
||||||
|
:group '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-address (cell)
|
||||||
|
"Get address field FIELD from MSG as a string or nil."
|
||||||
|
(let ((name (car cell)) (addr (cdr cell)))
|
||||||
|
(if name
|
||||||
|
(format "%s <%s>" name addr)
|
||||||
|
(format "%s" addr))))
|
||||||
|
|
||||||
|
(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"))))
|
||||||
|
(org-store-link-props
|
||||||
|
:type "mu4e"
|
||||||
|
:date date
|
||||||
|
:from (when from
|
||||||
|
(mu4e~org-address from))
|
||||||
|
:maildir (plist-get msg :maildir)
|
||||||
|
:message-id msgid
|
||||||
|
:path (plist-get msg :path)
|
||||||
|
:subject (plist-get msg :subject)
|
||||||
|
:to (when to
|
||||||
|
(mu4e~org-address to))
|
||||||
|
:link (concat "mu4e:msgid:" msgid)
|
||||||
|
:description (or (plist-get msg :subject) "No subject"))))
|
||||||
|
|
||||||
|
(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."
|
||||||
|
(when (derived-mode-p 'mu4e-view-mode 'mu4e-headers-mode)
|
||||||
|
(if (and (derived-mode-p 'mu4e-headers-mode)
|
||||||
|
mu4e-org-link-query-in-headers-mode)
|
||||||
|
(mu4e~org-store-link-query)
|
||||||
|
(when (mu4e-message-at-point)
|
||||||
|
(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-headers-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
|
|
@ -0,0 +1,501 @@
|
||||||
|
;;; mu4e-proc.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2011-2020 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-vars)
|
||||||
|
(require 'mu4e-utils)
|
||||||
|
(require 'mu4e-meta)
|
||||||
|
|
||||||
|
;;; Internal vars
|
||||||
|
|
||||||
|
(defvar mu4e~proc-buf nil
|
||||||
|
"Buffer (string) for data received from the backend.")
|
||||||
|
(defconst mu4e~proc-name " *mu4e-proc*"
|
||||||
|
"Name of the server process, buffer.")
|
||||||
|
(defvar mu4e~proc-process nil
|
||||||
|
"The mu-server process.")
|
||||||
|
|
||||||
|
;; dealing with the length cookie that precedes expressions
|
||||||
|
(defconst mu4e~cookie-pre "\376"
|
||||||
|
"Each expression starts with a length cookie:
|
||||||
|
<`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'>.")
|
||||||
|
(defconst mu4e~cookie-post "\377"
|
||||||
|
"Each expression starts with a length cookie:
|
||||||
|
<`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'>.")
|
||||||
|
(defconst mu4e~cookie-matcher-rx
|
||||||
|
(concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)" mu4e~cookie-post)
|
||||||
|
"Regular expression matching the length cookie.
|
||||||
|
Match 1 will be the length (in hex).")
|
||||||
|
|
||||||
|
;;; Functions
|
||||||
|
|
||||||
|
(defun mu4e~proc-running-p ()
|
||||||
|
"Whether the mu process is running."
|
||||||
|
(and mu4e~proc-process
|
||||||
|
(memq (process-status mu4e~proc-process)
|
||||||
|
'(run open listen connect stop))
|
||||||
|
t))
|
||||||
|
|
||||||
|
(defsubst mu4e~proc-eat-sexp-from-buf ()
|
||||||
|
"'Eat' the next s-expression from `mu4e~proc-buf'.
|
||||||
|
Note: this is a string, not an emacs-buffer. `mu4e~proc-buf gets
|
||||||
|
its contents from the mu-servers in the following form:
|
||||||
|
<`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'>
|
||||||
|
Function returns this sexp, or nil if there was none.
|
||||||
|
`mu4e~proc-buf' is updated as well, with all processed sexp data
|
||||||
|
removed."
|
||||||
|
(ignore-errors ;; the server may die in the middle...
|
||||||
|
;; mu4e~cookie-matcher-rx:
|
||||||
|
;; (concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)]" mu4e~cookie-post)
|
||||||
|
(let ((b (string-match mu4e~cookie-matcher-rx mu4e~proc-buf))
|
||||||
|
(sexp-len) (objcons))
|
||||||
|
(when b
|
||||||
|
(setq sexp-len (string-to-number (match-string 1 mu4e~proc-buf) 16))
|
||||||
|
;; does mu4e~proc-buf contain the full sexp?
|
||||||
|
(when (>= (length mu4e~proc-buf) (+ sexp-len (match-end 0)))
|
||||||
|
;; clear-up start
|
||||||
|
(setq mu4e~proc-buf (substring mu4e~proc-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~proc-buf 0 sexp-len)
|
||||||
|
'utf-8 t)))
|
||||||
|
(when objcons
|
||||||
|
(setq mu4e~proc-buf (substring mu4e~proc-buf sexp-len))
|
||||||
|
(car objcons)))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defun mu4e~proc-filter (_proc str)
|
||||||
|
"Filter string STR from PROC.
|
||||||
|
This processes the 'mu server' output. It accumulates the
|
||||||
|
strings into valid sexps by checking of the ';;eox' `end-of-sexp'
|
||||||
|
marker, and then evaluating them.
|
||||||
|
|
||||||
|
The server output is as follows:
|
||||||
|
|
||||||
|
1. an error
|
||||||
|
(:error 2 :message \"unknown command\")
|
||||||
|
;; eox
|
||||||
|
=> passed to `mu4e-error-func'.
|
||||||
|
|
||||||
|
2a. a message sexp looks something like:
|
||||||
|
\(
|
||||||
|
: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/1312254065_3.32282.pluto,4cd5bd4e9:2,\"
|
||||||
|
:priority high
|
||||||
|
:flags (new unread)
|
||||||
|
:attachments ((2 \"hello.jpg\" \"image/jpeg\") (3 \"laah.mp3\" \"audio/mp3\"))
|
||||||
|
:body-txt \" <message body>\"
|
||||||
|
\)
|
||||||
|
;; eox
|
||||||
|
=> this will be passed to `mu4e-header-func'.
|
||||||
|
|
||||||
|
2b. After the list of message sexps 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'.
|
||||||
|
|
||||||
|
4. a database update looks like:
|
||||||
|
(:update <msg-sexp> :move <nil-or-t>)
|
||||||
|
|
||||||
|
=> 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'."
|
||||||
|
(mu4e-log 'misc "* Received %d byte(s)" (length str))
|
||||||
|
(setq mu4e~proc-buf (concat mu4e~proc-buf str)) ;; update our buffer
|
||||||
|
(let ((sexp (mu4e~proc-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 :date)
|
||||||
|
(funcall mu4e-header-func sexp))
|
||||||
|
|
||||||
|
;; 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)
|
||||||
|
(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)))
|
||||||
|
|
||||||
|
;; do something with a temporary file
|
||||||
|
((plist-get sexp :temp)
|
||||||
|
(funcall mu4e-temp-func
|
||||||
|
(plist-get sexp :temp) ;; name of the temp file
|
||||||
|
(plist-get sexp :what) ;; what to do with it
|
||||||
|
;; (pipe|emacs|open-with...)
|
||||||
|
(plist-get sexp :docid) ;; docid of the message
|
||||||
|
(plist-get sexp :param)));; parameter for the action
|
||||||
|
|
||||||
|
;; 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~proc-eat-sexp-from-buf))))))
|
||||||
|
|
||||||
|
(defun mu4e~escape (str)
|
||||||
|
"Escape string STR for transport.
|
||||||
|
Put it in quotes, and escape existing quotation. In particular,
|
||||||
|
backslashes and double-quotes."
|
||||||
|
(let ((esc (replace-regexp-in-string "\\\\" "\\\\\\\\" str)))
|
||||||
|
(format "\"%s\"" (replace-regexp-in-string "\"" "\\\\\"" esc))))
|
||||||
|
|
||||||
|
(defun mu4e~proc-start ()
|
||||||
|
"Start the mu server process."
|
||||||
|
|
||||||
|
;; 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)))
|
||||||
|
|
||||||
|
(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~proc-buf "")
|
||||||
|
(setq mu4e~proc-process (apply 'start-process
|
||||||
|
mu4e~proc-name mu4e~proc-name
|
||||||
|
mu4e-mu-binary args))
|
||||||
|
;; register a function for (:info ...) sexps
|
||||||
|
(unless mu4e~proc-process
|
||||||
|
(mu4e-error "Failed to start the mu4e backend"))
|
||||||
|
(set-process-query-on-exit-flag mu4e~proc-process nil)
|
||||||
|
(set-process-coding-system mu4e~proc-process 'binary 'utf-8-unix)
|
||||||
|
(set-process-filter mu4e~proc-process 'mu4e~proc-filter)
|
||||||
|
(set-process-sentinel mu4e~proc-process 'mu4e~proc-sentinel)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-kill ()
|
||||||
|
"Kill the mu server process."
|
||||||
|
(let* ((buf (get-buffer mu4e~proc-name))
|
||||||
|
(proc (and (buffer-live-p buf) (get-buffer-process buf))))
|
||||||
|
(when proc
|
||||||
|
(let ((delete-exited-processes t))
|
||||||
|
(mu4e~call-mu '(quit)))
|
||||||
|
;; try sending SIGINT (C-c) to process, so it can exit gracefully
|
||||||
|
(ignore-errors
|
||||||
|
(signal-process proc 'SIGINT))))
|
||||||
|
(setq
|
||||||
|
mu4e~proc-process nil
|
||||||
|
mu4e~proc-buf nil))
|
||||||
|
|
||||||
|
;; error codes are defined in src/mu-util
|
||||||
|
;;(defconst mu4e-xapian-empty 19 "Error code: xapian is empty/non-existent")
|
||||||
|
|
||||||
|
(defun mu4e~proc-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~proc-process nil)
|
||||||
|
(setq mu4e~proc-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~call-mu (form)
|
||||||
|
"Call 'mu' with some command."
|
||||||
|
(unless (mu4e~proc-running-p) (mu4e~proc-start))
|
||||||
|
(let* ((print-length nil) (print-level nil)
|
||||||
|
(cmd (format "%S" form)))
|
||||||
|
(mu4e-log 'to-server "%s" cmd)
|
||||||
|
(process-send-string mu4e~proc-process (concat cmd "\n"))))
|
||||||
|
|
||||||
|
(defun mu4e~docid-msgid-param (docid-or-msgid)
|
||||||
|
"Construct a backend parameter based on DOCID-OR-MSGID."
|
||||||
|
(if (stringp docid-or-msgid)
|
||||||
|
`(:msgid ,(mu4e~escape docid-or-msgid))
|
||||||
|
`(:docid ,docid-or-msgid)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-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~call-mu `(add :path ,path)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-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~call-mu `(compose
|
||||||
|
:type ,type
|
||||||
|
:decrypt ,(and decrypt t)
|
||||||
|
:docid ,docid)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-contacts (personal after tstamp)
|
||||||
|
"Ask for contacts with PERSONAL AFTER 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)."
|
||||||
|
(mu4e~call-mu `(contacts
|
||||||
|
:personal ,(and personal t)
|
||||||
|
:after ,(or after nil)
|
||||||
|
:tstamp ,(or tstamp nil))))
|
||||||
|
|
||||||
|
(defun mu4e~proc-extract (action docid index decrypt
|
||||||
|
&optional path what param)
|
||||||
|
"Perform ACTION on part with DOCID INDEX DECRYPT PATH WHAT PARAM.
|
||||||
|
Use a message with DOCID and perform ACTION on it (as symbol,
|
||||||
|
either `save', `open', `temp') which mean:
|
||||||
|
* save: save the part to PATH (a path) (non-optional for save)
|
||||||
|
* open: open the part with the default application registered for doing so
|
||||||
|
* temp: save to a temporary file, then respond with
|
||||||
|
(:temp <path> :what <what> :param <param>)."
|
||||||
|
(mu4e~call-mu `(extract
|
||||||
|
:action ,action
|
||||||
|
:docid ,docid
|
||||||
|
:index ,index
|
||||||
|
:decrypt ,(and decrypt t)
|
||||||
|
:path ,path
|
||||||
|
:what ,what
|
||||||
|
:param ,param)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-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 will be called for, resp., a message (header row)
|
||||||
|
or an error."
|
||||||
|
(mu4e~call-mu `(find
|
||||||
|
:query ,query
|
||||||
|
:threads ,threads
|
||||||
|
:sortfield ,sortfield
|
||||||
|
:descending ,(if (eq sortdir 'descending) t nil)
|
||||||
|
:maxnum ,maxnum
|
||||||
|
:skip-dups ,skip-dups
|
||||||
|
:include-related ,include-related)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-index (&optional cleanup lazy-check)
|
||||||
|
"Index messages with possible CLEANUP and LAZY-CHECK."
|
||||||
|
(mu4e~call-mu `(index :cleanup ,cleanup :lazy-check ,lazy-check)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-mkdir (path)
|
||||||
|
"Create a new maildir-directory at filesystem PATH."
|
||||||
|
;;(mu4e~proc-send-command "cmd:mkdir path:%s" (mu4e~escape path))
|
||||||
|
(mu4e~call-mu `(mkdir :path ,path)))
|
||||||
|
|
||||||
|
|
||||||
|
(defun mu4e~proc-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, don't 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~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~proc-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~call-mu `(ping :queries ,queries)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-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~call-mu `(remove :docid ,docid)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-sent (path)
|
||||||
|
"Add the message at PATH to the database.
|
||||||
|
|
||||||
|
if this works, we will receive (:info add :path <path> :docid
|
||||||
|
<docid> :fcc <path>)."
|
||||||
|
(mu4e~call-mu `(sent :path ,path)))
|
||||||
|
|
||||||
|
(defun mu4e~proc-view (docid-or-msgid &optional mark-as-read decrypt verify)
|
||||||
|
"Get a message DOCID-OR-MSGID.
|
||||||
|
Optionally, if MARK-AS-READ is non-nil, the backend marks the message as
|
||||||
|
read before returning, if it was not already unread.
|
||||||
|
DECRYPT and VERIFY if necessary. The result will be delivered to
|
||||||
|
the function registered as `mu4e-view-func'."
|
||||||
|
(mu4e~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 ,mark-as-read
|
||||||
|
:extract-images ,(if mu4e-view-show-images t nil)
|
||||||
|
:decrypt ,(and decrypt t)
|
||||||
|
:verify ,(and verify t))))
|
||||||
|
|
||||||
|
(defun mu4e~proc-view-path (path &optional images decrypt verify)
|
||||||
|
"View message at PATH..
|
||||||
|
Optionally, if IMAGES is non-nil, backend will any images
|
||||||
|
attached to the message, and return them as temp files. The
|
||||||
|
result will be delivered to the function registered as
|
||||||
|
`mu4e-view-func'. Optionally DECRYPT and VERIFY."
|
||||||
|
(mu4e~call-mu `(view
|
||||||
|
:path ,path
|
||||||
|
:extract-images ,(if images t nil)
|
||||||
|
:decrypt ,(and decrypt t)
|
||||||
|
:verify ,(and verify t))))
|
||||||
|
|
||||||
|
;;; _
|
||||||
|
(provide 'mu4e-proc)
|
||||||
|
;;; mu4e-proc.el ends here
|
|
@ -0,0 +1,134 @@
|
||||||
|
;;; mu4e-speedbar --- Speedbar support for mu4e -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2012-2020 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-utils)
|
||||||
|
|
||||||
|
(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-headers-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-headers-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
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,642 @@
|
||||||
|
;;; mu4e-view-common.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 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 define common utils for 'old' and 'gnus' view mode.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'mu4e-utils) ;; utility functions
|
||||||
|
(require 'mu4e-vars)
|
||||||
|
(require 'mu4e-headers)
|
||||||
|
(require 'mu4e-mark)
|
||||||
|
(require 'mu4e-proc)
|
||||||
|
(require 'mu4e-compose)
|
||||||
|
(require 'mu4e-actions)
|
||||||
|
(require 'mu4e-message)
|
||||||
|
|
||||||
|
(require 'comint)
|
||||||
|
(require 'browse-url)
|
||||||
|
(require 'button)
|
||||||
|
(require 'epa)
|
||||||
|
(require 'epg)
|
||||||
|
(require 'thingatpt)
|
||||||
|
|
||||||
|
;;; Options
|
||||||
|
|
||||||
|
(defcustom mu4e-view-scroll-to-next t
|
||||||
|
"Move to the next message when calling
|
||||||
|
`mu4e-view-scroll-up-or-next' (typically bound to SPC) when at
|
||||||
|
the end of a message. 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
|
||||||
|
'( ("capture message" . mu4e-action-capture-message)
|
||||||
|
("view as pdf" . mu4e-action-view-as-pdf)
|
||||||
|
("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))
|
||||||
|
|
||||||
|
|
||||||
|
;;; Old options
|
||||||
|
|
||||||
|
;; These don't do anything useful when in "gnus" mode, except for avoid errors
|
||||||
|
;; for people that have these in their config.
|
||||||
|
|
||||||
|
(defcustom mu4e-view-show-addresses nil
|
||||||
|
"Whether to initially show full e-mail addresses for contacts.
|
||||||
|
Otherwise, just show their names. Ignored when using the gnus-based view."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'mu4e-view)
|
||||||
|
|
||||||
|
(make-obsolete-variable 'mu4e-view-wrap-lines nil "0.9.9-dev7")
|
||||||
|
(make-obsolete-variable 'mu4e-view-hide-cited nil "0.9.9-dev7")
|
||||||
|
|
||||||
|
(defcustom mu4e-view-date-format "%c"
|
||||||
|
"Date format to use in the message view.
|
||||||
|
In the format of `format-time-string'. Ignored when using the gnus-based view."
|
||||||
|
:type 'string
|
||||||
|
:group 'mu4e-view)
|
||||||
|
|
||||||
|
(defcustom mu4e-view-image-max-width 800
|
||||||
|
"The maximum width for images to display.
|
||||||
|
This is only effective if you're using an Emacs with Imagemagick
|
||||||
|
support, and `mu4e-view-show-images' is non-nil. Ignored when
|
||||||
|
using the gnus-based view."
|
||||||
|
:type 'integer
|
||||||
|
:group 'mu4e-view)
|
||||||
|
|
||||||
|
(defcustom mu4e-view-image-max-height 600
|
||||||
|
"The maximum height for images to display.
|
||||||
|
This is only effective if you're using an Emacs with Imagemagick
|
||||||
|
support, and `mu4e-view-show-images' is non-nil. Ignored when
|
||||||
|
using the gnus-based view."
|
||||||
|
:type 'integer
|
||||||
|
:group 'mu4e-view)
|
||||||
|
|
||||||
|
|
||||||
|
(defcustom mu4e-save-multiple-attachments-without-asking nil
|
||||||
|
"If non-nil, saving multiple attachments asks once for a
|
||||||
|
directory and saves all attachments in the chosen directory.
|
||||||
|
Ignored when using the gnus-based view."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'mu4e-view)
|
||||||
|
|
||||||
|
(defcustom mu4e-view-attachment-assoc nil
|
||||||
|
"Alist of (EXTENSION . PROGRAM).
|
||||||
|
Specify which PROGRAM to use to open attachment with EXTENSION.
|
||||||
|
Args EXTENSION and PROGRAM should be specified as strings.
|
||||||
|
Ignored when using the gnus-based view."
|
||||||
|
:group 'mu4e-view
|
||||||
|
:type '(alist :key-type string :value-type string))
|
||||||
|
|
||||||
|
(defcustom mu4e-view-attachment-actions
|
||||||
|
'( ("ssave" . mu4e-view-save-attachment-single)
|
||||||
|
("Ssave multi" . mu4e-view-save-attachment-multi)
|
||||||
|
("wopen-with" . mu4e-view-open-attachment-with)
|
||||||
|
("ein-emacs" . mu4e-view-open-attachment-emacs)
|
||||||
|
("dimport-in-diary" . mu4e-view-import-attachment-diary)
|
||||||
|
("kimport-public-key" . mu4e-view-import-public-key)
|
||||||
|
("|pipe" . mu4e-view-pipe-attachment))
|
||||||
|
"List of actions to perform on message attachments.
|
||||||
|
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 two arguments: the message
|
||||||
|
plist and the attachment number.
|
||||||
|
The first letter of NAME is used as a shortcut character.
|
||||||
|
Ignored when using the gnus-based view."
|
||||||
|
:group 'mu4e-view
|
||||||
|
:type '(alist :key-type string :value-type function))
|
||||||
|
|
||||||
|
;;; Keymaps
|
||||||
|
|
||||||
|
(defvar mu4e-view-header-field-keymap
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
(define-key map [mouse-1] 'mu4e~view-header-field-fold)
|
||||||
|
(define-key map (kbd "TAB") 'mu4e~view-header-field-fold)
|
||||||
|
map)
|
||||||
|
"Keymap used for header fields. Ignored when using the
|
||||||
|
gnus-based view.")
|
||||||
|
|
||||||
|
(defvar mu4e-view-contacts-header-keymap
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
(define-key map [mouse-2] 'mu4e~view-compose-contact)
|
||||||
|
(define-key map "C" 'mu4e~view-compose-contact)
|
||||||
|
(define-key map "c" 'mu4e~view-copy-contact)
|
||||||
|
map)
|
||||||
|
"Keymap used for the contacts in the header fields.
|
||||||
|
Ignored when using the gnus-based view.")
|
||||||
|
|
||||||
|
(defvar mu4e-view-attachments-header-keymap
|
||||||
|
(let ((map (make-sparse-keymap)))
|
||||||
|
(define-key map [mouse-1] 'mu4e~view-open-attach-from-binding)
|
||||||
|
(define-key map [?\M-\r] 'mu4e~view-open-attach-from-binding)
|
||||||
|
(define-key map [mouse-2] 'mu4e~view-save-attach-from-binding)
|
||||||
|
(define-key map (kbd "<S-return>") 'mu4e~view-save-attach-from-binding)
|
||||||
|
map)
|
||||||
|
"Keymap used in the \"Attachments\" header field. Ignored when
|
||||||
|
using the gnus-based view.")
|
||||||
|
|
||||||
|
;; 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-field-at-point :path))
|
||||||
|
(buf (get-buffer-create mu4e~view-raw-buffer-name)))
|
||||||
|
(unless (and path (file-readable-p path))
|
||||||
|
(mu4e-error "Not a readable file: %S" path))
|
||||||
|
(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-field (mu4e-message-at-point) :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 connected to
|
||||||
|
this view."
|
||||||
|
`(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 in the headers buffer
|
||||||
|
connected with this message view. 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 in the headers buffer
|
||||||
|
connected with this message view. 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 (when 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 in the headers
|
||||||
|
buffer connected with this message view. 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 in the headers
|
||||||
|
buffer connected with this message view. If this succeeds, return
|
||||||
|
the new docid. Otherwise, return nil."
|
||||||
|
(interactive)
|
||||||
|
(mu4e~view-prev-or-next-unread nil))
|
||||||
|
|
||||||
|
|
||||||
|
;;; 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 ()
|
||||||
|
"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)
|
||||||
|
"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)
|
||||||
|
"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
|
||||||
|
(call-interactively 'mu4e-headers-search-narrow)))
|
||||||
|
|
||||||
|
(defun mu4e-view-search-edit ()
|
||||||
|
"Run `mu4e-headers-search-edit' in the headers buffer."
|
||||||
|
(interactive)
|
||||||
|
(mu4e~view-in-headers-context (mu4e-headers-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)
|
||||||
|
(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 '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 can't scroll-up
|
||||||
|
anymore, 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 http:/https:/mailto:
|
||||||
|
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 '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, and ensure
|
||||||
|
it is valid. 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 to url(s). If MULTI (prefix-argument) is nil, go to
|
||||||
|
a single one, otherwise, 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(s) 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(s). 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)
|
||||||
|
"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 url NUM in the current message, prompting the
|
||||||
|
user with PROMPT."
|
||||||
|
(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.
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
(provide 'mu4e-view-common)
|
|
@ -0,0 +1,625 @@
|
||||||
|
;;; mu4e-view-gnus.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 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 define mu4e-view-mode (+ helper functions), which is used for
|
||||||
|
;; viewing e-mail messages
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'mu4e-view-common)
|
||||||
|
(require 'calendar)
|
||||||
|
(require 'gnus-art)
|
||||||
|
|
||||||
|
;;; Variables
|
||||||
|
|
||||||
|
(defvar gnus-icalendar-additional-identities)
|
||||||
|
(defvar helm-comp-read-use-marked)
|
||||||
|
(defvar-local mu4e~view-rendering nil)
|
||||||
|
|
||||||
|
(make-obsolete-variable 'mu4e-view-blocked-images 'gnus-blocked-images
|
||||||
|
"1.5.12")
|
||||||
|
(make-obsolete-variable '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-gnus (msg)
|
||||||
|
"View MSG using Gnus' article mode."
|
||||||
|
(when (bufferp gnus-article-buffer)
|
||||||
|
(kill-buffer gnus-article-buffer))
|
||||||
|
(with-current-buffer (get-buffer-create gnus-article-buffer)
|
||||||
|
(let ((inhibit-read-only t))
|
||||||
|
(erase-buffer)
|
||||||
|
(insert-file-contents-literally
|
||||||
|
(mu4e-message-field msg :path) 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 message as a string, for replying/forwarding
|
||||||
|
etc."
|
||||||
|
;; we need this for replying/forwarding, since the mu4e-compose
|
||||||
|
;; wants it that way.
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents-literally
|
||||||
|
(mu4e-message-field msg :path) nil nil nil t)
|
||||||
|
(mu4e~view-render-buffer msg)
|
||||||
|
(buffer-substring-no-properties (point-min) (point-max))))
|
||||||
|
|
||||||
|
(defun mu4e-action-view-in-browser (msg)
|
||||||
|
"Show current message MSG in browser, if it contains an html body."
|
||||||
|
;; (with-temp-buffer
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents-literally
|
||||||
|
(mu4e-message-field msg :path) nil nil nil t)
|
||||||
|
(run-hooks 'gnus-article-decode-hook)
|
||||||
|
(let ((header (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~view-render-buffer (msg)
|
||||||
|
"Render current buffer with MSG using Gnus' article mode in
|
||||||
|
buffer BUF."
|
||||||
|
(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)))
|
||||||
|
(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)))
|
||||||
|
|
||||||
|
(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-gnus-display-mime (msg)
|
||||||
|
"Same as `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)))
|
||||||
|
(cl-case field
|
||||||
|
((:path :maildir :user-agent :mailing-list :message-id)
|
||||||
|
(mu4e~view-gnus-insert-header field fieldval))
|
||||||
|
((: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)))
|
||||||
|
((:subject :to :from :cc :bcc :from-or-to :date :attachments
|
||||||
|
:signature :decryption)) ; handled by Gnus
|
||||||
|
(t
|
||||||
|
(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 in Gnus article view."
|
||||||
|
(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 the custom FIELD in Gnus article view."
|
||||||
|
(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)
|
||||||
|
"Do not trigger an error when displaying an ical attachment
|
||||||
|
with no 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 ()
|
||||||
|
(or (eq major-mode 'mu4e-view-mode)
|
||||||
|
(derived-mode-p '(mu4e-view-mode))))
|
||||||
|
|
||||||
|
(defun mu4e~view-nop (func &rest args)
|
||||||
|
"Do nothing 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)
|
||||||
|
"Advice 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)
|
||||||
|
"Advice 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-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 "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 "s" #'mu4e-headers-search)
|
||||||
|
(define-key map "S" #'mu4e-view-search-edit)
|
||||||
|
(define-key map "/" #'mu4e-view-search-narrow)
|
||||||
|
|
||||||
|
(define-key map (kbd "<M-left>") #'mu4e-headers-query-prev)
|
||||||
|
(define-key map (kbd "<M-right>") #'mu4e-headers-query-next)
|
||||||
|
|
||||||
|
(define-key map "b" #'mu4e-headers-search-bookmark)
|
||||||
|
(define-key map "B" #'mu4e-headers-search-bookmark-edit)
|
||||||
|
|
||||||
|
(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-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)
|
||||||
|
|
||||||
|
(define-key map ";" #'mu4e-context-switch)
|
||||||
|
|
||||||
|
;; 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 "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 (kbd "M-q") 'article-fill-long-lines)
|
||||||
|
|
||||||
|
;; 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)))
|
||||||
|
|
||||||
|
(set-keymap-parent map special-mode-map)
|
||||||
|
map)
|
||||||
|
"Keymap for mu4e-view mode")
|
||||||
|
|
||||||
|
(set-keymap-parent mu4e-view-mode-map button-buffer-map)
|
||||||
|
(suppress-keymap mu4e-view-mode-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)
|
||||||
|
|
||||||
|
(defvar mu4e-view-mode-abbrev-table nil)
|
||||||
|
|
||||||
|
(defun mu4e~view-mode-body ()
|
||||||
|
"Body of the mode-function."
|
||||||
|
(use-local-map mu4e-view-mode-map)
|
||||||
|
(mu4e-context-in-modeline)
|
||||||
|
(setq buffer-undo-list t);; don't record undo info
|
||||||
|
;; autopair mode gives error when pressing RET
|
||||||
|
;; turn it off
|
||||||
|
(when (boundp 'autopair-dont-activate)
|
||||||
|
(setq autopair-dont-activate t)))
|
||||||
|
|
||||||
|
;; "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)
|
||||||
|
(mu4e~view-mode-body))
|
||||||
|
|
||||||
|
;;; 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))
|
||||||
|
"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
|
||||||
|
|
||||||
|
|
||||||
|
(defun mu4e~view-gather-mime-parts ()
|
||||||
|
"Gather all MIME parts as an alist that 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 \",\".
|
||||||
|
|
||||||
|
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 (cdr (assoc 'filename (assoc "attachment" (cdr part))))))
|
||||||
|
(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 (expand-file-name f dir))))
|
||||||
|
(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 :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))
|
||||||
|
|
||||||
|
"Actions for MIME-parts. Each 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 N 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 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 (cdr-safe (assoc 'filename (assoc "attachment" (cdr handle)))))
|
||||||
|
(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."
|
||||||
|
(let* ((opener
|
||||||
|
(pcase system-type
|
||||||
|
(`darwin "open")
|
||||||
|
((or 'gnu 'gnu/linux 'gnu/kfreebsd) "xdg-open")))
|
||||||
|
(prog (if (or force-ask (not opener))
|
||||||
|
(read-shell-command "Open MIME-part with: ")
|
||||||
|
opener)))
|
||||||
|
(call-process prog nil 0 nil file)))
|
||||||
|
|
||||||
|
(defun mu4e-view-mime-part-action (&optional n)
|
||||||
|
"Apply some action on mime-part N in the current messsage.
|
||||||
|
If N is not specified, ask for it. N can be supplied as a
|
||||||
|
prefix-argument, and note that one does not need to prefix that
|
||||||
|
with C-u.
|
||||||
|
|
||||||
|
I.e., '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 (cl-find-if (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 found for action %S" action)))
|
||||||
|
(receives (or (plist-get action :receives)
|
||||||
|
(mu4e-error "No :receives 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))))))))
|
||||||
|
|
||||||
|
;;;
|
||||||
|
(provide 'mu4e-view-gnus)
|
||||||
|
;;; mu4e-view.el ends here
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,69 @@
|
||||||
|
;;; mu4e-view.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 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 define mu4e-view-mode (+ helper functions), which is used for
|
||||||
|
;; viewing e-mail messages
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
(declare-function mu4e~view-gnus "mu4e-view-gnus")
|
||||||
|
(declare-function mu4e~view-old "mu4e-view-old")
|
||||||
|
(declare-function mu4e~headers-update-handler "mu4e-headers")
|
||||||
|
(declare-function mu4e-headers-search "mu4e-headers")
|
||||||
|
(declare-function mu4e-error "mu4e-utils")
|
||||||
|
|
||||||
|
(require 'mu4e-view-common)
|
||||||
|
(require (if mu4e-view-use-old 'mu4e-view-old 'mu4e-view-gnus))
|
||||||
|
|
||||||
|
(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.
|
||||||
|
|
||||||
|
Depending on the value of `mu4e-view-use-old', either use mu4e's
|
||||||
|
internal display mode, or a (by default) display mode based on
|
||||||
|
Gnus' article-mode."
|
||||||
|
|
||||||
|
;; sanity checks.
|
||||||
|
(if (and mu4e-view-use-old (featurep 'mu4e-view-gnus))
|
||||||
|
(error "Cannot use old view when gnus-view is loaded; restart emacs")
|
||||||
|
(if (and (not mu4e-view-use-old) (featurep 'mu4e-view-old))
|
||||||
|
(error "Cannot use gnus-based view with old view loaded; restart emacs")))
|
||||||
|
|
||||||
|
(mu4e~headers-update-handler msg nil nil);; update headers, if necessary.
|
||||||
|
|
||||||
|
(if mu4e-view-use-old
|
||||||
|
(mu4e~view-old msg)
|
||||||
|
(mu4e~view-gnus msg)))
|
||||||
|
|
||||||
|
(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-headers-search (concat "msgid:" msgid) nil nil t msgid t))
|
||||||
|
|
||||||
|
(provide 'mu4e-view)
|
||||||
|
;;; mu4e-view.el ends here
|
|
@ -0,0 +1,67 @@
|
||||||
|
;;; mu4e.el --- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2011-2019 Dirk-Jan C. Binnema
|
||||||
|
|
||||||
|
;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
;; Keywords: email
|
||||||
|
;; 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:
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'mu4e-vars)
|
||||||
|
(require 'mu4e-headers) ;; headers view
|
||||||
|
(require 'mu4e-view) ;; message view
|
||||||
|
(require 'mu4e-main) ;; main screen
|
||||||
|
(require 'mu4e-compose) ;; message composition / sending
|
||||||
|
(require 'mu4e-proc) ;; communication with backend
|
||||||
|
(require 'mu4e-utils) ;; utility functions
|
||||||
|
(require 'mu4e-context) ;; support for contexts
|
||||||
|
|
||||||
|
(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)))
|
||||||
|
|
||||||
|
|
||||||
|
;;;###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~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)))
|
||||||
|
|
||||||
|
;;; _
|
||||||
|
(provide 'mu4e)
|
||||||
|
;;; mu4e.el ends here
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,234 @@
|
||||||
|
;;; 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-utils")
|
||||||
|
(declare-function mu4e-message "mu4e-message")
|
||||||
|
(declare-function mu4e-compose-mode "mu4e-compose")
|
||||||
|
|
||||||
|
|
||||||
|
;;; 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
|
|
@ -0,0 +1,4 @@
|
||||||
|
@set UPDATED 17 July 2021
|
||||||
|
@set UPDATED-MONTH July 2021
|
||||||
|
@set EDITION 1.6.1
|
||||||
|
@set VERSION 1.6.1
|
|
@ -0,0 +1,4 @@
|
||||||
|
@set UPDATED 17 July 2021
|
||||||
|
@set UPDATED-MONTH July 2021
|
||||||
|
@set EDITION 1.6.1
|
||||||
|
@set VERSION 1.6.1
|
Loading…
Reference in New Issue