This commit is contained in:
opfez 2022-07-18 12:11:38 +02:00
parent d76764d98d
commit a26811626d
33 changed files with 24721 additions and 0 deletions

66
elisp/mu4e/Makefile.am Normal file
View File

@ -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

1223
elisp/mu4e/Makefile.in Normal file

File diff suppressed because it is too large Load Diff

19
elisp/mu4e/TODO Normal file
View File

@ -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:

451
elisp/mu4e/fdl.texi Normal file
View File

@ -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:

15
elisp/mu4e/mu4e-about.org Normal file
View File

@ -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]

253
elisp/mu4e/mu4e-actions.el Normal file
View File

@ -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

1114
elisp/mu4e/mu4e-compose.el Normal file

File diff suppressed because it is too large Load Diff

183
elisp/mu4e/mu4e-context.el Normal file
View File

@ -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

224
elisp/mu4e/mu4e-contrib.el Normal file
View File

@ -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

604
elisp/mu4e/mu4e-draft.el Normal file
View File

@ -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

2002
elisp/mu4e/mu4e-headers.el Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

102
elisp/mu4e/mu4e-lists.el Normal file
View File

@ -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

387
elisp/mu4e/mu4e-main.el Normal file
View File

@ -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

470
elisp/mu4e/mu4e-mark.el Normal file
View File

@ -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

364
elisp/mu4e/mu4e-message.el Normal file
View File

@ -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

11
elisp/mu4e/mu4e-meta.el Normal file
View File

@ -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)

View File

@ -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)

127
elisp/mu4e/mu4e-org.el Normal file
View File

@ -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

501
elisp/mu4e/mu4e-proc.el Normal file
View File

@ -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

134
elisp/mu4e/mu4e-speedbar.el Normal file
View File

@ -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

1381
elisp/mu4e/mu4e-utils.el Normal file

File diff suppressed because it is too large Load Diff

1202
elisp/mu4e/mu4e-vars.el Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -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

1097
elisp/mu4e/mu4e-view-old.el Normal file

File diff suppressed because it is too large Load Diff

69
elisp/mu4e/mu4e-view.el Normal file
View File

@ -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

67
elisp/mu4e/mu4e.el Normal file
View File

@ -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

5823
elisp/mu4e/mu4e.info Normal file

File diff suppressed because it is too large Load Diff

5101
elisp/mu4e/mu4e.texi Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

4
elisp/mu4e/stamp-vti Normal file
View File

@ -0,0 +1,4 @@
@set UPDATED 17 July 2021
@set UPDATED-MONTH July 2021
@set EDITION 1.6.1
@set VERSION 1.6.1

4
elisp/mu4e/version.texi Normal file
View File

@ -0,0 +1,4 @@
@set UPDATED 17 July 2021
@set UPDATED-MONTH July 2021
@set EDITION 1.6.1
@set VERSION 1.6.1