diff --git a/.clang-format b/.clang-format deleted file mode 100644 index 621eb83f..00000000 --- a/.clang-format +++ /dev/null @@ -1,39 +0,0 @@ ---- -AlignAfterOpenBracket: 'Align' -AlignConsecutiveAssignments: 'false' -AlignConsecutiveDeclarations: 'false' -AlignConsecutiveMacros: 'true' -AlignOperands: 'true' -AlignTrailingComments: 'true' -AllowAllArgumentsOnNextLine: 'false' -AllowAllParametersOfDeclarationOnNextLine: 'false' -AllowShortBlocksOnASingleLine: 'false' -AllowShortCaseLabelsOnASingleLine: 'false' -AllowShortFunctionsOnASingleLine: 'Inline' -AllowShortIfStatementsOnASingleLine: 'false' -AlwaysBreakAfterReturnType: 'All' -BinPackParameters: 'false' -BinPackArguments: 'false' -BreakBeforeBraces: 'Linux' -ColumnLimit: '100' -DerivePointerAlignment: 'false' -IndentCaseLabels: 'false' -IndentWidth: '8' -KeepEmptyLinesAtTheStartOfBlocks: 'false' -Language: 'Cpp' -MaxEmptyLinesToKeep: '1' -PointerAlignment: 'Right' -SortIncludes: 'false' -SpaceAfterCStyleCast: 'false' -SpaceBeforeAssignmentOperators : 'true' -SpaceBeforeParens: 'ControlStatements' -SpaceInEmptyParentheses: 'false' -SpacesInSquareBrackets: 'false' -TabWidth: '8' -UseTab: 'AlignWithSpaces' -PenaltyBreakAssignment: '3' -PenaltyBreakBeforeFirstCallParameter: '15' - -# Local Variables: -# mode: yaml -# End: diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 00000000..3a491ef9 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,2 @@ +((emacs-lisp-mode + (indent-tabs-mode . nil))) diff --git a/.editorconfig b/.editorconfig index 28ae07e7..d7b5d392 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,6 @@ # editorconfig file (see EditorConfig.org), with some # lowest-denominator settings that should work for many editors. - root = true # this is the top-level [*] @@ -11,20 +10,23 @@ insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true -# The "best" answer is "tabs-for-indentation; spaces for alignment". +# The "best" answer is "tabs-for-indentation; spaces for alignment", but in +# practice that's hard to accomplish in many editors. +# +# So we use spaces instead, at least that looks consistent for all [*.{cc,cpp,hh,hpp}] -indent_style = tab +indent_style = space indent_size = 8 max_line_length = 100 [*.{c,h}] -indent_style = tab +indent_style = space indent_size = 8 max_line_length = 80 [configure.ac] -indent_style = tab +indent_style = space indent_size = 4 max_line_length = 100 diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0793144b..b26ee50f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -22,12 +22,12 @@ jobs: name: ubuntu-deps run: | sudo apt update - sudo apt-get install meson ninja-build libglib2.0-dev libxapian-dev libgmime-3.0-dev pkg-config + sudo apt-get install automake autoconf-archive autotools-dev libglib2.0-dev libxapian-dev libgmime-3.0-dev m4 make libtool pkg-config - if: contains(matrix.os, 'macos') name: macos-deps run: | - brew install meson ninja libgpg-error libtool pkg-config glib gmime xapian + brew install autoconf automake libgpg-error libtool pkg-config gettext glib gmime xapian - name: configure run: ./autogen.sh --disable-guile diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..063324ac --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +language: c +sudo: required +compiler: + - gcc +env: + global: + - BUILD_PKGS="libtool autoconf autoconf-archive automake texinfo" + - BUILD_LIBS="libgmime-2.6-dev libxapian-dev guile-2.0-dev libwebkitgtk-dev" + - TEST_PKGS="pmccabe" + matrix: + - EVM_EMACS=emacs-24.1-bin + - EVM_EMACS=emacs-24.2-bin + - EVM_EMACS=emacs-24.3-bin + # - EVM_EMACS=emacs-24.5-travis + # - EVM_EMACS=emacs-25.1-travis +before_install: + - git submodule update --init --recursive + # The Ubuntu version on travis is way too old, need Autoconf 2.69 + - sudo add-apt-repository ppa:dns/gnu -y + - sudo apt-get -qq update + - sudo apt-get install -qq ${BUILD_PKGS} ${BUILD_LIBS} ${TEST_PKGS} +install: + - sudo mkdir /usr/local/evm + - sudo chown $(id -u):$(id -g) /usr/local/evm + - curl -fsSkL https://raw.github.com/rejeep/evm/master/go | bash + - export PATH="$HOME/.evm/bin:$PATH" + - evm install $EVM_EMACS --use +script: + # Need recent version of autoconf-archive + - curl http://nl.mirror.babylon.network/gnu/autoconf-archive/autoconf-archive-2016.09.16.tar.xz -o /tmp/aa.tar.xz && tar xf /tmp/aa.tar.xz + - cp autoconf-archive-2016.09.16/m4/*.m4 m4/ + - ./autogen.sh + - ./configure + - make + - make check diff --git a/HACKING b/HACKING new file mode 100644 index 00000000..59b1d0ea --- /dev/null +++ b/HACKING @@ -0,0 +1,130 @@ +* HACKING + + Here are some guidelines for hacking on the 'mu' source code. + + This is a fairly long list -- this is not meant to discourage anyone from + working on mu; I think most of the rules are common sense anyway, and some of + the more stylistic-aesthetic rules are clearly visible in current source code, + so as long as any new code 'fits in', it should go a long way in satisfying + them. + + I should add some notes for the Lisp/Scheme code as well... + +** Coding style + + For consistency and, more important, to keep things understandable, mu + attempts to follow the following rules: + + 1. Basic code layout is like in the Linux kernel coding style. Keep the '{' + on the same line as the statement, except for functions. We're slowly + moving to use SPC for indentation: all new code should use that. + + While TABs are techically better, it seems that using SPCs is harder to + get wrong. + + 2. Lines should not exceed 100 characters + + 3. Functions should be kept short. + + 4. Source files should not exceed 1000 lines, with few exceptions. + + 5. Non-static C-functions have the prefix based on their module, e.g., + ~mu-foo.h~ declares a function of 'mu_foo_bar (int a);', mu-foo.c implements + this. C++ functions use the Mu namespace + + 6. Non-global functions *don't* have the module prefix, and are declared + static. + + 7. Functions have their return type on a separate line before the function + name, so: + +#+BEGIN_EXAMPLE + static int + foo (const char *bar) + { + .... + } +#+END_EXAMPLE + + 8. In C code, variable-declarations are at the beginning of a block. + + In C code, the declaration does *not* initialize the variable. This will + give the compiler a chance to warn us if the variable is not initialized + in a certain code path. Exception: autoptr & friends. + + 9. Returned strings of type ~char*~ must be freed by the caller; if they are + not to be freed, ~const char*~ should be used instead + + 10. Functions calls have a space between function name and arguments, unless + there are none, so: + + ~foo (12, 3)~; + + and + + ~bar();~ + + after a comma, a space should follow. + + 11. C-functions that do not take arguments are explicitly declared as f(void) + and not f(). Reason: f() means that the arguments are /unspecified/ (in C) + + 12. C-code should not use ~//~ comments. + + +** Logging + + For logging, mu uses the GLib logging functions/macros as listed below, + except when logging may not have been initialized. + + The logging system redirects most logging to the log file (typically, + =~/.cache/mu/mu.log, or to the systemd journal=). ~g_critical~ messages are + written to stderr. + + - ~g_message~ is for non-error messages the user will see (unless running with + ~--quiet~) + - ~g_warning~ is for problems the user may be able to do something about (and + they are written on ~stderr~) + - ~g_critical~ is for mu bugs, serious, internal problems (~g_return_if_fail~ and + friends use this). (and they are written on ~stderr~) + - don't use ~g_error~ + +** Compiling from git + + For hacking, you're strongly advised to use the latest git version. + Compilation from git should be straightforward, if you have the right tools + installed. + +*** dependencies + + You need to install a few dependencies; e.g. on Debian/Ubuntu: +#+BEGIN_EXAMPLE + sudo apt-get install \ + automake \ + autoconf-archive \ + autotools-dev \ + libglib2.0-dev \ + libxapian-dev \ + libgmime-3.0-dev \ + m4 \ + make \ + libtool \ + pkg-config +#+END_EXAMPLE + + Then, to compile straight from ~git~: + +#+BEGIN_EXAMPLE + $ git clone https://github.com/djcb/mu + $ cd mu + $ ./autogen.sh + $ make +#+END_EXAMPLE + + You only need to run ~./autogen.sh~ the first time and after changes in the + build system; otherwise you can use ~./configure~. + +# Local Variables: +# mode: org; org-startup-folded: nofold +# fill-column: 80 +# End: diff --git a/Makefile.am b/Makefile.am index 1120fb33..641300dc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -44,6 +44,7 @@ tags: EXTRA_DIST= \ TODO \ + HACKING \ README.org \ gtest.mk \ NEWS \ diff --git a/Makefile.meson b/Makefile.meson deleted file mode 100644 index 381ae319..00000000 --- a/Makefile.meson +++ /dev/null @@ -1,43 +0,0 @@ -## Copyright (C) 2008-2021 Dirk-Jan C. Binnema -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 3 of the License, or -## (at your option) any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with this program; if not, write to the Free Software Foundation, -## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - - -# Makefile with some useful targets for meson/ninja - -NINJA ?= ninja -BUILDDIR ?= $(CURDIR)/build -MESON ?= meson - -all: $(BUILDDIR) - $(NINJA) -C $(BUILDDIR) - -$(BUILDDIR): - $(MESON) $(BUILDDIR) - -check: test - -test: $(BUILDDIR) - @cd $(BUILDDIR); $(MESON) test - -dist: $(BUILDDIR) - @cd $(BUILDDIR); $(MESON) dist - -install: $(BUILDDIR) - @cd $(BUILDDIR); $(MESON) install - -clean: - @test -d $(BUILDDIR) && $(NINJA) -C $(BUILDDIR) clean - diff --git a/NEWS.org b/NEWS.org index 1d405908..c3592ce8 100644 --- a/NEWS.org +++ b/NEWS.org @@ -1,54 +1,6 @@ #+STARTUP:showall * NEWS (user visible changes & bigger non-visible ones) -* 1.7 (unreleased, development) - -*** mu - - - ~mu~ switched to use the [[https://mesonbuild.com][meson]] build system by default. The existing - ~autotools~ is still avaiable, but is to be removed after the 1.8 release. - - Using ~meson~ (which you need to install), you can use something like in the - mu top directory: -#+begin example - $ meson build && ninja -C build -#+end example - - After that, ~ninja -C build~ should be enough. ~autogen.sh~ has been updated - to use ~meson~ instead, and and even delivers a ~Makefile~ with some useful - targets that invoke the corresponding ~meson~ / ~ninja~ targets. - - - ~mu~ now require C++17 support for building (this is in preparation for code - that actually _uses_ C++17) - -*** mu4e - - - the old mu4e-view is gone; only the gnus-based one remains, this allowed - for removing quite a bit of old code. - - - a lot of the internals have been changed: - - - ~mu4e~ is slowly moving from using the ~~~ to the more common ~--~ separator for - private functions; i.e., ~mu4e~foo~ becomes ~mu4e--foo~. - - ~mu4e-utils.el~ had become a bit of a dumping ground for bits of code; - it's gone now, with the functionality move to topic-specific files -- - ~mu4e-folders.el~, ~mu4e-bookmarks.el~, ~mu4e-update.el~, and included in - existing files. - - the remaining common functionality has ended up in ~mu4e-helpers.el~ - - ~mu4e-search.el~ takes the search-specific code from ~mu4e-headers.el~, and - adds a minor-mode for the keybindings. - - ~mu4e-context.el~ and ~mu4e-update.el~ also define minor modes with - keybindings, which saves a lot of code in the various views, since they - don't need explicitly bind all those function. - - also ~mu4e-vars.el~ had become very big, we're refactoring the factories - ~defvar~ / ~defcustom~ declarations to the topic-specific files. - -*** guile - - - the guile interface has been deprecated. It may be revamped at some point, - but will be different from the curren one. To be removed after 1.8.x - - * 1.6 (released, as of July 27 2021) NOTE: After upgrading, you need to call ~mu init~, with your prefered parameters diff --git a/autogen.sh b/autogen.sh index 555cdb2d..1155320f 100755 --- a/autogen.sh +++ b/autogen.sh @@ -1,29 +1,34 @@ #!/bin/sh # Run this to generate all the initial makefiles, etc. -echo "*** meson build setup" - test -f mu/mu.cc || { echo "*** Run this script from the top-level mu source directory" exit 1 } -BUILDDIR=build +# opportunistically; usually not needed, but occasionally it'll +# avoid build errors that would otherwise confuse users. +test -f Makefile && { + echo "*** clear out old things" + make distclean 2> /dev/null +} -command -v meson 2> /dev/null + +command -V autoreconf > /dev/null if [ $? != 0 ]; then - echo "*** No meson found, please install it ***" + echo "*** No autoreconf found, please install it ***" exit 1 fi -# we could remove build/ but let's avoid rm -rf risks... -if test -d ${BUILDDIR}; then - meson --reconfigure ${BUILDDIR} +rm -f config.cache +rm -rf autom4te.cache + +autoreconf --force --install --verbose || exit $? + +if test -z "$*"; then + echo "# Configuring without parameters" else - meson ${BUILDDIR} $@ + echo "# Configure with parameters $*" fi -# Add a Makefile with some useful target -cp Makefile.meson Makefile - -echo "*** Now run 'ninja -C ${BUILDDIR}' to build mu" +./configure --config-cache $@ diff --git a/configure.ac b/configure.ac index 1c0e82e0..c2e091a1 100644 --- a/configure.ac +++ b/configure.ac @@ -15,7 +15,7 @@ ## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. AC_PREREQ([2.68]) -AC_INIT([mu],[1.7.0],[https://github.com/djcb/mu/issues],[mu]) +AC_INIT([mu],[1.6.5],[https://github.com/djcb/mu/issues],[mu]) AC_COPYRIGHT([Copyright (C) 2008-2021 Dirk-Jan C. Binnema]) AC_CONFIG_HEADERS([config.h]) AC_CONFIG_SRCDIR([mu/mu.cc]) @@ -42,15 +42,15 @@ AC_PROG_CC_C99 AC_PROG_INSTALL AC_HEADER_STDC -extra_flags="-Wformat-security \ +extra_flags="-Wformat-security \ -Wstack-protector \ -Wstack-protector-all \ -Wno-cast-function-type \ -Wno-bad-function-cast" -AX_CXX_COMPILE_STDCXX_17 +AX_CXX_COMPILE_STDCXX_14 AX_COMPILER_FLAGS_CXXFLAGS([],[],[${extra_cflags}]) -AX_APPEND_COMPILE_FLAGS([-Wno-inline],[CXXFLAGS]) +AX_APPEND_COMPILE_FLAGS([-Wno-inline ],[CXXFLAGS]) AX_VALGRIND_CHECK LT_INIT @@ -363,10 +363,6 @@ AS_IF([test "x$ac_cv_header_wordexp_h" != "xyes"],[ echo " --maildir=/home/user/Maildir" ]) - -echo "NOTE: autotools support has been deprecated and will be removed" -echo " after the next stable release. use the meson build instead" - echo echo "Now, type 'make' (or 'gmake') to build mu" echo diff --git a/lib/mu-server.cc b/lib/mu-server.cc index d390dcbb..08546d2b 100644 --- a/lib/mu-server.cc +++ b/lib/mu-server.cc @@ -95,6 +95,7 @@ struct Server::Private { void add_handler (const Parameters& params); void compose_handler (const Parameters& params); void contacts_handler (const Parameters& params); + void extract_handler (const Parameters& params); void find_handler (const Parameters& params); void help_handler (const Parameters& params); void index_handler (const Parameters& params); @@ -213,6 +214,23 @@ Server::Private::make_command_map () "return changes since tstamp" }}}, "get contact information", [&](const auto& params){contacts_handler(params);}}); + + cmap.emplace("extract", + CommandInfo{ + ArgMap{{":docid", ArgInfo{Type::Number, true, "document for the message" }}, + {":index", ArgInfo{Type::Number, true, + "index for the part to operate on" }}, + {":action", ArgInfo{Type::Symbol, true, "what to do with the part" }}, + {":decrypt", ArgInfo{Type::Symbol, false, + "whether to decrypt encrypted parts (if any)" }}, + {":path", ArgInfo{Type::String, false, + "part for saving (for action: save)" }}, + {":what", ArgInfo{Type::Symbol, false, + "what to do with the part (feedback)" }}, + {":param", ArgInfo{Type::String, false, "parameter for 'what'" }}}, + "extract mime-parts from a message", + [&](const auto& params){extract_handler(params);}}); + cmap.emplace("find", CommandInfo{ ArgMap{ {":query", ArgInfo{Type::String, true, "search expression" }}, @@ -303,6 +321,13 @@ Server::Private::make_command_map () {":path", ArgInfo{Type::String, false, "message filesystem path"}}, {":mark-as-read", ArgInfo{Type::Symbol, false, "mark message as read (if not already)"}}, + {":extract-images", ArgInfo{Type::Symbol, false, + "whether to extract images for this messages (if any)"}}, + {":decrypt", ArgInfo{Type::Symbol, false, + "whether to decrypt encrypted parts (if any)" }}, + {":verify", ArgInfo{Type::Symbol, false, + "whether to verify signatures (if any)" }} + }, "view a message. exactly one of docid/msgid/path must be specified", [&](const auto& params){view_handler(params);}}); @@ -354,10 +379,15 @@ Server::Private::invoke (const std::string& expr) noexcept static MuMsgOptions message_options (const Parameters& params) { + const auto extract_images{get_bool_or(params, ":extract-images", false)}; const auto decrypt{get_bool_or(params, ":decrypt", false)}; + const auto verify{get_bool_or(params, ":verify", false)}; int opts{MU_MSG_OPTION_NONE}; - + if (extract_images) + opts |= MU_MSG_OPTION_EXTRACT_IMAGES; + if (verify) + opts |= MU_MSG_OPTION_VERIFY | MU_MSG_OPTION_USE_AGENT; if (decrypt) opts |= MU_MSG_OPTION_DECRYPT | MU_MSG_OPTION_USE_AGENT; @@ -513,6 +543,123 @@ Server::Private::contacts_handler (const Parameters& params) output_sexp(std::move(seq)); } + +static Sexp::List +save_part (MuMsg *msg, unsigned docid, unsigned index, + MuMsgOptions opts, const Parameters& params) +{ + const auto path{get_string_or(params, ":path")}; + if (path.empty()) + throw Error{Error::Code::Command, "missing path"}; + + GError *gerr{}; + if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | (int)MU_MSG_OPTION_OVERWRITE), + path.c_str(), index, &gerr)) + throw Error{Error::Code::File, &gerr, "failed to save part"}; + + Sexp::List seq; + seq.add_prop(":info", Sexp::make_symbol("save")); + seq.add_prop(":message", Sexp::make_string(format("%s has been saved", path.c_str()))); + + return seq; +} + + +static Sexp::List +open_part (MuMsg *msg, unsigned docid, unsigned index, MuMsgOptions opts) +{ + GError *gerr{}; + char *targetpath{mu_msg_part_get_cache_path (msg, opts, index, &gerr)}; + if (!targetpath) + throw Error{Error::Code::File, &gerr, "failed to get cache-path"}; + + if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING), + targetpath, index, &gerr)) { + g_free(targetpath); + throw Error{Error::Code::File, &gerr, "failed to save to cache-path"}; + } + + if (!mu_util_play (targetpath, TRUE,/*allow local*/ + FALSE/*allow remote*/, &gerr)) { + g_free(targetpath); + throw Error{Error::Code::File, &gerr, "failed to play"}; + } + + Sexp::List seq; + seq.add_prop(":info", Sexp::make_symbol("open")); + seq.add_prop(":message", Sexp::make_string(format("%s has been opened", targetpath))); + g_free (targetpath); + + return seq; +} + +static Sexp::List +temp_part (MuMsg *msg, unsigned docid, unsigned index, + MuMsgOptions opts, const Parameters& params) +{ + const auto what{get_symbol_or(params, ":what")}; + if (what.empty()) + throw Error{Error::Code::Command, "missing 'what'"}; + + const auto param{get_string_or(params, ":param")}; + + GError *gerr{}; + char *path{mu_msg_part_get_cache_path (msg, opts, index, &gerr)}; + if (!path) + throw Error{Error::Code::File, &gerr, "could not get cache path"}; + + if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING), + path, index, &gerr)) { + g_free(path); + throw Error{Error::Code::File, &gerr, "saving failed"}; + } + + Sexp::List lst; + lst.add_prop(":temp", Sexp::make_string(path)); + lst.add_prop(":what", Sexp::make_string(what)); + lst.add_prop(":docid", Sexp::make_number(docid)); + + if (!param.empty()) + lst.add_prop(":param", Sexp::make_string(param)); + + g_free(path); + return lst; +} + + + +/* 'extract' extracts some mime part from a message */ +void +Server::Private::extract_handler (const Parameters& params) +{ + const auto docid{get_int_or(params, ":docid")}; + const auto index{get_int_or(params, ":index")}; + const auto opts{message_options(params)}; + + auto msg{store().find_message(docid)}; + if (!msg) + throw Error{Error::Code::Store, "failed to get message"}; + + try { + const auto action{get_symbol_or(params, ":action")}; + if (action == "save") + output_sexp(save_part (msg, docid, index, opts, params)); + else if (action == "open") + output_sexp(open_part (msg, docid, index, opts)); + else if (action == "temp") + output_sexp(temp_part (msg, docid, index, opts, params)); + else { + throw Error{Error::Code::InvalidArgument, + "unknown action '%s'", action.c_str()}; + } + + } catch (...) { + mu_msg_unref (msg); + throw; + } +} + + /* get a *list* of all messages with the given message id */ static std::vector docids_for_msgid (const Query& q, const std::string& msgid, size_t max=100) @@ -1073,7 +1220,7 @@ Server::Private::view_handler (const Parameters& params) Sexp::List seq; seq.add_prop(":view", build_message_sexp( - msg, docid, {}, MU_MSG_OPTION_NONE)); + msg, docid, {}, message_options(params))); mu_msg_unref(msg); output_sexp (std::move(seq)); } diff --git a/m4/Makefile.am b/m4/Makefile.am index 9b3829b0..27a49eeb 100644 --- a/m4/Makefile.am +++ b/m4/Makefile.am @@ -1,4 +1,4 @@ -## Copyright (C) 2008-2021 Dirk-Jan C. Binnema +## Copyright (C) 2008-2020 Dirk-Jan C. Binnema ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by @@ -36,7 +36,6 @@ EXTRA_DIST= \ ax_compiler_flags_ldflags.m4 \ ax_cxx_compile_stdcxx.m4 \ ax_cxx_compile_stdcxx_14.m4 \ - ax_cxx_compile_stdcxx_17.m4 \ ax_file_escapes.m4 \ ax_is_release.m4 \ ax_lib_readline.m4 \ diff --git a/m4/m4_ax_cxx_compile_stdcxx_17.m4 b/m4/m4_ax_cxx_compile_stdcxx_17.m4 deleted file mode 100644 index a6834171..00000000 --- a/m4/m4_ax_cxx_compile_stdcxx_17.m4 +++ /dev/null @@ -1,35 +0,0 @@ -# ============================================================================= -# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_17.html -# ============================================================================= -# -# SYNOPSIS -# -# AX_CXX_COMPILE_STDCXX_17([ext|noext], [mandatory|optional]) -# -# DESCRIPTION -# -# Check for baseline language coverage in the compiler for the C++17 -# standard; if necessary, add switches to CXX and CXXCPP to enable -# support. -# -# This macro is a convenience alias for calling the AX_CXX_COMPILE_STDCXX -# macro with the version set to C++17. The two optional arguments are -# forwarded literally as the second and third argument respectively. -# Please see the documentation for the AX_CXX_COMPILE_STDCXX macro for -# more information. If you want to use this macro, you also need to -# download the ax_cxx_compile_stdcxx.m4 file. -# -# LICENSE -# -# Copyright (c) 2015 Moritz Klammler -# Copyright (c) 2016 Krzesimir Nowak -# -# Copying and distribution of this file, with or without modification, are -# permitted in any medium without royalty provided the copyright notice -# and this notice are preserved. This file is offered as-is, without any -# warranty. - -#serial 2 - -AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX]) -AC_DEFUN([AX_CXX_COMPILE_STDCXX_17], [AX_CXX_COMPILE_STDCXX([17], [$1], [$2])]) diff --git a/meson.build b/meson.build index 3b370397..114eea51 100644 --- a/meson.build +++ b/meson.build @@ -18,14 +18,14 @@ # project setup # project('mu', ['c', 'cpp'], - version: '1.7.0', + version: '1.6.5', meson_version: '>= 0.52.0', # debian 10 license: 'GPL-3.0-or-later', default_options : [ 'buildtype=debugoptimized', 'warning_level=1', 'c_std=c11', - 'cpp_std=c++17' + 'cpp_std=c++14' ] ) @@ -51,6 +51,7 @@ extra_flags = [ '-Winit-self', '-Wmissing-include-dirs', '-Wpointer-arith', + #'-Wswitch-enum', '-Wswitch-default', ] diff --git a/mu4e/Makefile.am b/mu4e/Makefile.am index 790a0e69..3239da63 100644 --- a/mu4e/Makefile.am +++ b/mu4e/Makefile.am @@ -23,26 +23,26 @@ mu4e_TEXINFOS=fdl.texi dist_lisp_LISP= \ mu4e-actions.el \ - mu4e-bookmarks.el \ mu4e-compose.el \ mu4e-context.el \ mu4e-contrib.el \ mu4e-draft.el \ - mu4e-folders.el \ mu4e-headers.el \ mu4e-icalendar.el \ mu4e-lists.el \ - mu4e-helpers.el \ mu4e-main.el \ mu4e-mark.el \ mu4e-message.el \ mu4e-meta.el \ mu4e-org.el \ - mu4e-server.el \ + mu4e-proc.el \ mu4e-speedbar.el \ - mu4e-update.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 diff --git a/mu4e/meson.build b/mu4e/meson.build index ea82af39..d8fb0db3 100644 --- a/mu4e/meson.build +++ b/mu4e/meson.build @@ -23,21 +23,18 @@ mu4e_meta = configure_file( 'VERSION' : meson.project_version(), # project_build_root() with meson >= 0.56 'abs_top_builddir': join_paths(meson.build_root()), - 'MU_DOC_DIR' : join_paths(datadir, 'doc', 'mu'), + 'MU_DOC_DIR' : join_paths(datadir, 'doc', 'mu') }) mu4e_srcs=[ 'mu4e-actions.el', - 'mu4e-bookmarks.el', 'mu4e-compose.el', 'mu4e-context.el', 'mu4e-contrib.el', 'mu4e-draft.el', - 'mu4e-folders.el', 'mu4e.el', 'mu4e-headers.el', - 'mu4e-helpers.el', 'mu4e-icalendar.el', 'mu4e-lists.el', 'mu4e-main.el', @@ -45,21 +42,17 @@ mu4e_srcs=[ 'mu4e-message.el', join_paths(meson.current_build_dir(), 'mu4e-meta.el'), 'mu4e-org.el', - 'mu4e-server.el', + 'mu4e-proc.el', 'mu4e-speedbar.el', - 'mu4e-update.el', + 'mu4e-utils.el', 'mu4e-vars.el', 'mu4e-view.el', + 'mu4e-view-common.el', + 'mu4e-view-gnus.el', + 'mu4e-view-old.el', 'obsolete/org-mu4e.el', ] - -# -# with (development) emacs 28.50 / native compilation this doesn't -# _quite_ work; see: https://debbugs.gnu.org/db/47/47987.html -# -lispdir = join_paths(datadir, 'emacs', 'site-lisp', 'mu4e') - foreach src : mu4e_srcs target_name= '@BASENAME@.elc' target_path = join_paths(meson.current_build_dir(), target_name) @@ -68,8 +61,6 @@ foreach src : mu4e_srcs build_by_default: true, input: src, output: target_name, - install_dir: lispdir, - install: true, command: [emacs, '--no-init-file', '--batch', @@ -79,9 +70,6 @@ foreach src : mu4e_srcs '--funcall', 'batch-byte-compile', '@INPUT@']) endforeach -# also install the sources. -install_data(mu4e_srcs, install_dir: lispdir) - if makeinfo.found() custom_target('mu4e_info', diff --git a/mu4e/mu4e-actions.el b/mu4e/mu4e-actions.el index e8c19166..2c8af24e 100644 --- a/mu4e/mu4e-actions.el +++ b/mu4e/mu4e-actions.el @@ -30,11 +30,18 @@ (require 'cl-lib) (require 'ido) -(require 'mu4e-helpers) +(require 'mu4e-utils) (require 'mu4e-message) -(require 'mu4e-search) (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 @@ -170,14 +177,6 @@ Otherwise return nil." (if (re-search-forward regexp nil t) (replace-match to-string nil nil))))) -(declare-function mu4e--server-add "mu4e-server") -(defun mu4e--refresh-message (path) - "Re-parse message at PATH. -if this works, we will -receive (:info add :path :docid ) as well as (:update -)." - (mu4e--server-add path)) - (defun mu4e-action-retag-message (msg &optional retag-arg) "Change tags of MSG with RETAG-ARG. @@ -232,7 +231,7 @@ would add 'tag' and 'long tag', and remove 'oldtag'." path)) (mu4e-message (concat "tagging: " (mapconcat 'identity taglist ", "))) - (mu4e--refresh-message path))) + (mu4e-refresh-message path))) (defun mu4e-action-show-thread (msg) "Show thread for message at point with point remaining on MSG. @@ -241,13 +240,14 @@ action was invoked. If invoked in view mode, continue to display the message." (let ((msgid (mu4e-message-field msg :message-id))) (when msgid - (let ((mu4e-search-threads t) + (let ((mu4e-headers-show-threads t) (mu4e-headers-include-related t)) - (mu4e-search + (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 diff --git a/mu4e/mu4e-bookmarks.el b/mu4e/mu4e-bookmarks.el deleted file mode 100644 index ce6a2531..00000000 --- a/mu4e/mu4e-bookmarks.el +++ /dev/null @@ -1,150 +0,0 @@ -;;; mu4e-bookmarks.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 -;; Maintainer: 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 . - -;;; Commentary: - -;;; Code: -(require 'cl-lib) -(require 'mu4e-helpers) - - -;;; Configuration - -(defgroup mu4e-bookmarks nil - "Settings for bookmarks." - :group 'mu4e) - -;; for backward compatibility, when a bookmark was defined with defstruct. -(cl-defun make-mu4e-bookmark (&key name query key) - "Create a mu4e plist. -It has has with the following elements: -- NAME: the user-visible name of the bookmark -- KEY: a single key to search for this bookmark -- QUERY: the query for this bookmark. Either a literal string or a function - that evaluates to a string." - `(:name ,name :query ,query :key ,key)) -(make-obsolete 'make-mu4e-bookmark "`unneeded; `mu4e-bookmarks' -are plists" "1.3.7") - -(defcustom mu4e-bookmarks - '(( :name "Unread messages" - :query "flag:unread AND NOT flag:trashed" - :key ?u) - ( :name "Today's messages" - :query "date:today..now" - :key ?t) - ( :name "Last 7 days" - :query "date:7d..now" - :hide-unread t - :key ?w) - ( :name "Messages with images" - :query "mime:image/*" - :key ?p)) - "List of pre-defined queries that are shown on the main screen. - -Each of the list elements is a plist with at least: -`:name' - the name of the query -`:query' - the query expression or function -`:key' - the shortcut key. - -Note that the :query parameter can be a function/lambda. - -Optionally, you can add the following: -`:hide' - if t, the bookmark is hidden from the main-view and - speedbar. -`:hide-unread' - do not show the counts of unread/total number - of matches for the query in the main-view. This can be useful -if a bookmark uses a very slow query. :hide-unread -is implied from :hide. Furthermore, it is implied if -`:query' is a function. - -Queries used to determine the unread/all counts do _not_ apply -`mu4e-query-rewrite-function'; nor do they discard duplicate or -unreadable messages (for efficiency). Thus, the numbers shown may -differ from the number you get from a 'real' query." - :type '(repeat (plist)) - :version "1.3.9" - :group 'mu4e-bookmarks) - - - (defun mu4e-ask-bookmark (prompt) - "Ask the user for a bookmark (using PROMPT) as defined in -`mu4e-bookmarks', then return the corresponding query." - (unless (mu4e-bookmarks) (mu4e-error "No bookmarks defined")) - (let* ((prompt (mu4e-format "%s" prompt)) - (bmarks - (mapconcat - (lambda (bm) - (concat - "[" (propertize (make-string 1 (plist-get bm :key)) - 'face 'mu4e-highlight-face) - "]" - (plist-get bm :name))) (mu4e-bookmarks) ", ")) - (kar (read-char (concat prompt bmarks)))) - (mu4e-get-bookmark-query kar))) - -(defun mu4e-get-bookmark-query (kar) - "Get the corresponding bookmarked query for shortcut KAR. -Raise an error if none is found." - (let* ((chosen-bm - (or (cl-find-if - (lambda (bm) - (= kar (plist-get bm :key))) - (mu4e-bookmarks)) - (mu4e-warn "Unknown shortcut '%c'" kar))) - (expr (plist-get chosen-bm :query)) - (expr (if (not (functionp expr)) expr - (funcall expr))) - (query (eval expr))) - (if (stringp query) - query - (mu4e-warn "Expression must evaluate to query string ('%S')" expr)))) - - -(defun mu4e-bookmark-define (query name key) - "Define a bookmark for QUERY with NAME and shortcut KEY. -Append it to `mu4e-bookmarks'. Replaces any existing bookmark -with KEY." - (setq mu4e-bookmarks - (cl-remove-if - (lambda (bm) - (= (plist-get bm :key) key)) - (mu4e-bookmarks))) - (cl-pushnew `(:name ,name - :query ,query - :key ,key) - mu4e-bookmarks :test 'equal)) - -(defun mu4e-bookmarks () - "Get `mu4e-bookmarks' in the (new) format. -Convert from the old format if needed." - (cl-map 'list - (lambda (item) - (if (and (listp item) (= (length item) 3)) - `(:name ,(nth 1 item) - :query ,(nth 0 item) - :key ,(nth 2 item)) - item)) - mu4e-bookmarks)) - -(provide 'mu4e-bookmarks) -;;; mu4e-bookmarks.el ends here diff --git a/mu4e/mu4e-compose.el b/mu4e/mu4e-compose.el index 3b94636a..d7b1478e 100644 --- a/mu4e/mu4e-compose.el +++ b/mu4e/mu4e-compose.el @@ -72,17 +72,192 @@ (require 'smtpmail) (require 'rfc2368) -(require 'mu4e-server) +(require 'mu4e-utils) +(require 'mu4e-vars) +(require 'mu4e-proc) (require 'mu4e-actions) (require 'mu4e-message) (require 'mu4e-draft) (require 'mu4e-context) - -;;; Configuration -;; see mu4e-drafts.el - +;;; Composing / Sending messages + +(defgroup mu4e-compose nil + "Customizations for composing/sending messages." + :group 'mu4e) + +(defcustom mu4e-sent-messages-behavior 'sent + "Determines what mu4e does with sent messages. + +This is one of the symbols: +* `sent' move the sent message to the Sent-folder (`mu4e-sent-folder') +* `trash' move the sent message to the Trash-folder (`mu4e-trash-folder') +* `delete' delete the sent message. + +Note, when using GMail/IMAP, you should set this to either +`trash' or `delete', since GMail already takes care of keeping +copies in the sent folder. + +Alternatively, `mu4e-sent-messages-behavior' can be a function +which takes no arguments, and which should return one of the mentioned +symbols, for example: + + (setq mu4e-sent-messages-behavior (lambda () + (if (string= (message-sendmail-envelope-from) \"foo@example.com\") + 'delete 'sent))) + +The various `message-' functions from `message-mode' are available +for querying the message information." + :type '(choice (const :tag "move message to mu4e-sent-folder" sent) + (const :tag "move message to mu4e-trash-folder" trash) + (const :tag "delete message" delete)) + :group 'mu4e-compose) + +(defcustom mu4e-compose-context-policy 'ask + "Policy for determining the context when composing a new message. + +If the value is `always-ask', ask the user unconditionally. + +In all other cases, if any context matches (using its match +function), this context is used. Otherwise, if none of the +contexts match, we have the following choices: + +- `pick-first': pick the first of the contexts available (ie. the default) +- `ask': ask the user +- `ask-if-none': ask if there is no context yet, otherwise leave it as it is +- nil: return nil; leaves the current context as is. + +Also see `mu4e-context-policy'." + :type '(choice + (const :tag "Always ask what context to use" always-ask) + (const :tag "Ask if none of the contexts match" ask) + (const :tag "Ask when there's no context yet" ask-if-none) + (const :tag "Pick the first context if none match" pick-first) + (const :tag "Don't change the context when none match" nil)) + :safe 'symbolp + :group 'mu4e-compose) + +(defcustom mu4e-compose-crypto-policy + '(encrypt-encrypted-replies sign-encrypted-replies) + "Policy to control when messages will be signed/encrypted. + +The value is a list, whose members determine the behaviour of +`mu4e~compose-crypto-message'. Specifically, it might contain: + +- `sign-all-messages': Always add a signature. +- `sign-new-messages': Add a signature to new message, ie. + messages that aren't responses to another message. +- `sign-forwarded-messages': Add a signature when forwarding + a message +- `sign-edited-messages': Add a signature to drafts +- `sign-all-replies': Add a signature when responding to + another message. +- `sign-plain-replies': Add a signature when responding to + non-encrypted messages. +- `sign-encrypted-replies': Add a signature when responding + to encrypted messages. + +It should be noted that certain symbols have priorities over one +another. So `sign-all-messages' implies `sign-all-replies', which +in turn implies `sign-plain-replies'. Adding both to the set, is +not a contradiction, but a redundant configuration. + +All `sign-*' options have a `encrypt-*' analogue." + :type '(set :greedy t + (const :tag "Sign all messages" sign-all-messages) + (const :tag "Encrypt all messages" encrypt-all-messages) + (const :tag "Sign new messages" sign-new-messages) + (const :tag "Encrypt new messages" encrypt-new-messages) + (const :tag "Sign forwarded messages" sign-forwarded-messages) + (const :tag "Encrypt forwarded messages" encrypt-forwarded-messages) + (const :tag "Sign edited messages" sign-edited-messages) + (const :tag "Encrypt edited messages" edited-forwarded-messages) + (const :tag "Sign all replies" sign-all-replies) + (const :tag "Encrypt all replies" encrypt-all-replies) + (const :tag "Sign replies to plain messages" sign-plain-replies) + (const :tag "Encrypt replies to plain messages" encrypt-plain-replies) + (const :tag "Sign replies to encrypted messages" sign-encrypted-replies) + (const :tag "Encrypt replies to encrypted messages" encrypt-encrypted-replies)) + :group 'mu4e-compose) + +(defcustom mu4e-compose-crypto-reply-encrypted-policy nil + "Policy for signing/encrypting replies to encrypted messages. +We have the following choices: + +- `sign': sign the reply +- `sign-and-encrypt': sign and encrypt the reply +- `encrypt': encrypt the reply, but don't sign it. +- anything else: do nothing." + :type '(choice + (const :tag "Sign the reply" sign) + (const :tag "Sign and encrypt the reply" sign-and-encrypt) + (const :tag "Encrypt the reply" encrypt) + (const :tag "Don't do anything" nil)) + :safe 'symbolp + :group 'mu4e-compose) + +(make-obsolete-variable 'mu4e-compose-crypto-reply-encrypted-policy "The use of the + 'mu4e-compose-crypto-reply-encrypted-policy' variable is deprecated. + 'mu4e-compose-crypto-policy' should be used instead" + "2020-03-06") + +(defcustom mu4e-compose-crypto-reply-plain-policy nil + "Policy for signing/encrypting replies to messages received unencrypted. +We have the following choices: + +- `sign': sign the reply +- `sign-and-encrypt': sign and encrypt the reply +- `encrypt': encrypt the reply, but don't sign it. +- anything else: do nothing." + :type '(choice + (const :tag "Sign the reply" sign) + (const :tag "Sign and encrypt the reply" sign-and-encrypt) + (const :tag "Encrypt the reply" encrypt) + (const :tag "Don't do anything" nil)) + :safe 'symbolp + :group 'mu4e-compose) + +(make-obsolete-variable 'mu4e-compose-crypto-reply-plain-policy "The use of the + 'mu4e-compose-crypto-reply-plain-policy' variable is deprecated. + 'mu4e-compose-crypto-policy' should be used instead" + "2020-03-06") + +(make-obsolete-variable 'mu4e-compose-crypto-reply-policy "The use of the + 'mu4e-compose-crypto-reply-policy' variable is deprecated. + 'mu4e-compose-crypto-reply-plain-policy' and + 'mu4e-compose-crypto-reply-encrypted-policy' should be used instead" + "2017-09-02") + +(defcustom mu4e-compose-format-flowed nil + "Whether to compose messages to be sent as format=flowed. +\(Or with long lines if variable `use-hard-newlines' is set to +nil). The variable `fill-flowed-encode-column' lets you customize +the width beyond which format=flowed lines are wrapped." + :type 'boolean + :safe 'booleanp + :group 'mu4e-compose) + +(defcustom mu4e-compose-pre-hook nil + "Hook run just *before* message composition starts. +If the compose-type is either 'reply' or 'forward', the variable +`mu4e-compose-parent-message' points to the message replied to / +being forwarded / edited, and `mu4e-compose-type' contains the +type of message to be composed. + +Note that there is no draft message yet when this hook runs, it +is meant for influencing the how mu4e constructs the draft +message. If you want to do something with the draft messages after +it has been constructed, `mu4e-compose-mode-hook' would be the +place to do that." + :type 'hook + :group 'mu4e-compose) + +(defvar mu4e-compose-type nil + "The compose-type for this buffer. +This is a symbol, `new', `forward', `reply' or `edit'.") + ;;; Attachments + (defun mu4e-compose-attach-message (msg) "Insert message MSG as an attachment." (let ((path (plist-get msg :path))) @@ -153,12 +328,12 @@ If needed, set the Fcc header, and register the handler function." (setq message-fcc-handler-function old-handler) ;; reset the fcc handler (let ((mdir-path (concat (mu4e-root-maildir) maildir))) ;; Create the full maildir structure for the sent folder if it doesn't exist. - ;; `mu4e--server-mkdir` runs asynchronously but no matter whether it runs before or after + ;; `mu4e~proc-mkdir` runs asynchronously but no matter whether it runs before or after ;; `write-file`, the sent maildir ends up in the correct state. (unless (file-exists-p mdir-path) - (mu4e--server-mkdir mdir-path))) + (mu4e~proc-mkdir mdir-path))) (write-file file) ;; writing maildirs files is easy - (mu4e--server-add file))))))) ;; update the database + (mu4e~proc-add file))))))) ;; update the database (defvar mu4e-compose-hidden-headers `("^References:" "^Face:" "^X-Face:" @@ -206,7 +381,7 @@ Message-ID." (set-buffer-modified-p nil) (mu4e-message "Saved (%d lines)" (count-lines (point-min) (point-max))) ;; update the file on disk -- ie., without the separator - (mu4e--server-add (buffer-file-name)))) + (mu4e~proc-add (buffer-file-name)))) ;;; address completion @@ -218,9 +393,9 @@ Message-ID." "Complete address STR with predication PRED for ACTION." (cond ((eq action nil) - (try-completion str mu4e--contacts-hash pred)) + (try-completion str mu4e~contacts-hash pred)) ((eq action t) - (all-completions str mu4e--contacts-hash pred)) + (all-completions str mu4e~contacts-hash pred)) ((eq action 'metadata) ;; our contacts are already sorted - just need to tell the ;; completion machinery not to try to undo that... @@ -272,6 +447,7 @@ removing the In-Reply-To header." (setq mu4e-compose-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-S-u") 'mu4e-update-mail-and-index) + (define-key map (kbd "C-c C-;") 'mu4e-compose-context-switch) (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index) (define-key map (kbd "C-c C-k") 'mu4e-message-kill-buffer) (define-key map (kbd "M-q") 'mu4e-fill-paragraph) @@ -332,12 +508,7 @@ buffers; lets remap its faces so it uses the ones for mu4e." \\{message-mode-map}." (progn (use-local-map mu4e-compose-mode-map) - - (mu4e-context-minor-mode) - (define-key mu4e-context-minor-mode-map (kbd ";") nil) - (define-key mu4e-context-minor-mode-map (kbd "C-c C-;") - #'mu4e-compose-context-switch) - + (mu4e-context-in-modeline) (set (make-local-variable 'message-signature) mu4e-compose-signature) ;; set this to allow mu4e to work when gnus-agent is unplugged in gnus (set (make-local-variable 'message-send-mail-real-function) nil) @@ -354,9 +525,8 @@ buffers; lets remap its faces so it uses the ones for mu4e." (mu4e~compose-register-message-save-hooks) ;; offer completion for e-mail addresses (when mu4e-compose-complete-addresses - (unless mu4e--contacts-hash - ;; work-around for https://github.com/djcb/mu/issues/1016 - (mu4e--request-contacts-maybe)) + (unless mu4e~contacts-hash ;; work-around for https://github.com/djcb/mu/issues/1016 + (mu4e~request-contacts-maybe)) (mu4e~compose-setup-completion)) (if mu4e-compose-format-flowed (progn @@ -432,7 +602,7 @@ buffers; lets remap its faces so it uses the ones for mu4e." (defun mu4e~set-sent-handler-message-sent-hook-fn () ;; mu4e~compose-mark-after-sending (setq mu4e-sent-func 'mu4e-sent-handler) - (mu4e--server-sent (buffer-file-name))) + (mu4e~proc-sent (buffer-file-name))) (defun mu4e-send-harden-newlines () "Set the hard property to all newlines." @@ -540,8 +710,8 @@ are optional." (set (make-local-variable 'mu4e-compose-type) compose-type) (put 'mu4e-compose-type 'permanent-local t) ;; maybe switch the context - (mu4e--context-autoswitch mu4e-compose-parent-message - mu4e-compose-context-policy) + (mu4e~context-autoswitch mu4e-compose-parent-message + mu4e-compose-context-policy) (run-hooks 'mu4e-compose-pre-hook) ;; this opens (or re-opens) a messages with all the basic headers set. @@ -660,7 +830,7 @@ when the buffer is in `mu4e-compose-mode': ;; Remove the <> (when (and msg-id (string-match "<\\(.*\\)>" msg-id)) (save-window-excursion - (mu4e--server-move (match-string 1 msg-id) mu4e-drafts-folder nil t) + (mu4e~proc-move (match-string 1 msg-id) mu4e-drafts-folder nil t) (kill-buffer buf))))) ;; Kill previous buffer which points to wrong file ;; No file, just change the buffer file name (setq buffer-file-name @@ -676,7 +846,7 @@ For Forwarded ('Passed') and Replied messages, try to set the appropriate flag at the message forwarded or replied-to." (mu4e~compose-set-parent-flag path) (when (file-exists-p path) ;; maybe the draft was not saved at all - (mu4e--server-remove docid)) + (mu4e~proc-remove docid)) ;; kill any remaining buffers for the draft file, or they will hang around... ;; this seems a bit hamfisted... (when message-kill-buffer-on-exit @@ -738,9 +908,9 @@ buffer." (setq forwarded-from (cl-first refs)))))) ;; remove the <> (when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to)) - (mu4e--server-move (match-string 1 in-reply-to) nil "+R-N")) + (mu4e~proc-move (match-string 1 in-reply-to) nil "+R-N")) (when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from)) - (mu4e--server-move (match-string 1 forwarded-from) nil "+P-N"))))))) + (mu4e~proc-move (match-string 1 forwarded-from) nil "+P-N"))))))) (defun mu4e-compose (compose-type) "Start composing a message of COMPOSE-TYPE. @@ -778,7 +948,7 @@ Symbol `edit' is only allowed for draft messages." (when (window-live-p viewwin) (select-window viewwin)))) ;; talk to the backend - (mu4e--server-compose compose-type decrypt docid))))) + (mu4e~proc-compose compose-type decrypt docid))))) (defun mu4e-compose-reply () "Compose a reply for the message at point in the headers buffer." @@ -812,8 +982,6 @@ draft message." ;; mu4e-compose-func and mu4e-send-func are wrappers so we can set ourselves ;; as default emacs mailer (define-mail-user-agent etc.) -(declare-function mu4e "mu4e") - ;;;###autoload (defun mu4e~compose-mail (&optional to subject other-headers _continue switch-function yank-action _send-actions _return-action) @@ -850,7 +1018,7 @@ caller. It has the form (FUNCTION . ARGS). The function is called after the mail has been sent or put aside, and the mail buffer buried." (unless (mu4e-running-p) - (mu4e)) + (mu4e~start)) ;; create a new draft message 'resetting' (as below) is not actually needed in this case, but ;; let's prepare for the re-edit case as well diff --git a/mu4e/mu4e-contacts.el b/mu4e/mu4e-contacts.el deleted file mode 100644 index 65d32e26..00000000 --- a/mu4e/mu4e-contacts.el +++ /dev/null @@ -1,231 +0,0 @@ -;;; mu4e-contacts.el -- part of mu4e -*- lexical-binding: t -*- - -;; Copyright (C) 2021 Dirk-Jan C. Binnema - -;; Author: Dirk-Jan C. Binnema -;; Maintainer: 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 . - -;;; Commentary: - -;; Utility functions used in the mu4e - -;;; Code: -(require 'cl-lib) -(require 'mu4e-helpers) - - -;;; Configuration -(defcustom mu4e-compose-complete-addresses t - "Whether to do auto-completion of e-mail addresses." - :type 'boolean - :group 'mu4e-compose) - -(defcustom mu4e-compose-complete-only-personal nil - "Whether to consider only 'personal' e-mail addresses for completion. -That is, addresses from messages where user was explicitly in one -of the address fields (this excludes mailing list messages). -These addresses are the ones specified with `mu init'." - :type 'boolean - :group 'mu4e-compose) - -(defcustom mu4e-compose-complete-only-after "2014-01-01" - "Consider only contacts last seen after this date. - -Date must be a string of the form YYY-MM-DD. - -This is useful for limiting a potentially enormous set of -contacts for auto-completion to just those that are present in -the e-mail corpus in recent timses. Set to nil to not have any -time-based restriction." - :type 'string - :group 'mu4e-compose) - -;; names and mail-addresses can be mapped onto their canonical -;; counterpart. use the customizeable function -;; mu4e-canonical-contact-function to do that. below the identity -;; function for mapping a contact onto the canonical one. -(defun mu4e-contact-identity (contact) - "Return the name and the mail-address of a CONTACT. -It is used as the identity function for converting contacts to -their canonical counterpart; useful as an example." - (let ((name (plist-get contact :name)) - (mail (plist-get contact :mail))) - (list :name name :mail mail))) - -(make-obsolete-variable 'mu4e-contact-rewrite-function - "mu4e-contact-process-function (see docstring)" - "mu4e 1.3.2") -(make-obsolete-variable 'mu4e-compose-complete-ignore-address-regexp - "mu4e-contact-process-function (see docstring)" - "mu4e 1.3.2") - -(defcustom mu4e-contact-process-function - (lambda(addr) ;; filter-out no-reply addresses - (unless (string-match-p "no[t]?[-\\.]?repl\\(y\\|ies\\)" addr) - addr)) - "Function for processing contact information for use in auto-completion. - -The function receives the contact as a string, e.g - \"Foo Bar \" - \"cuux@example.com\" - -The function should return either: -- nil: do not use this contact for completion -- the (possibly rewritten) address, which must be -an RFC-2822-compatible e-mail address." - :type 'function - :group 'mu4e-compose) - -(defcustom mu4e-compose-reply-ignore-address - '("no-?reply") - "Addresses to prune when doing wide replies. - -This can be a regexp matching the address, a list of regexps or a -predicate function. A value of nil keeps all the addresses." - :type '(choice - (const nil) - function - string - (repeat string)) - :group 'mu4e-compose) - - -;;; Internal variables -(defvar mu4e--contacts-tstamp "0" - "Timestamp for the most recent contacts update." ) - -(defvar mu4e--contacts-hash nil - "Hash that maps contacts (ie. 'name ') to an integer for sorting. -We need to keep this information around to quickly re-sort -subsets of the contacts in the completions function in -mu4e-compose.") - -;;; user mail address -(defun mu4e-personal-addresses(&optional no-regexp) - "Get the list user's personal addresses, as passed to mu init. -The address are either plain e-mail address or /regular - expressions/. When NO-REGEXP is non-nil, do not include regexp - address patterns (if any)." - (seq-remove - (lambda(addr) (and no-regexp (string-match-p "^/.*/" addr))) - (when (mu4e-server-properties) - (plist-get (mu4e-server-properties) :personal-addresses)))) - -(defun mu4e-personal-address-p (addr) - "Is ADDR a personal address? -Evaluate to nil if ADDR matches any of the personal addresses. -Uses (mu4e-personal-addresses) for the addresses with both the plain -addresses and /regular expressions/." - (when addr - (seq-find - (lambda (m) - (if (string-match "/\\(.*\\)/" m) - (let ((rx (match-string 1 m)) - (case-fold-search t)) - (if (string-match rx addr) t nil)) - (eq t (compare-strings addr nil nil m nil nil 'case-insensitive)))) - (mu4e-personal-addresses)))) - -(define-obsolete-function-alias 'mu4e-user-mail-address-p - 'mu4e-personal-address-p "1.5.5") - - -;; don't use the older vars anymore -(make-obsolete-variable 'mu4e-user-mail-address-regexp - 'mu4e-user-mail-address-list "0.9.9.x") -(make-obsolete-variable 'mu4e-my-email-addresses - 'mu4e-user-mail-address-list "0.9.9.x") -(make-obsolete-variable 'mu4e-user-mail-address-list - "determined by server; see `mu4e-personal-addresses'." - "1.3.8") - - -(defun mu4e--update-contacts (contacts &optional tstamp) - "Receive a sorted list of CONTACTS newer than TSTAMP. -Each of the contacts has the form - (FULL_EMAIL_ADDRESS . RANK) and fill `mu4e--contacts-hash' with -it, with each contact mapped to an integer for their ranking. - -This is used by the completion function in mu4e-compose." - ;; We have our nicely sorted list, map them to a list - ;; of increasing integers. We use that map in the composer - ;; to sort them there. It would have been so much easier if emacs - ;; allowed us to use the sorted-list as-is, but no such luck. - (let ((n 0)) - (unless mu4e--contacts-hash - (setq mu4e--contacts-hash (make-hash-table :test 'equal :weakness nil - :size (length contacts)))) - (dolist (contact contacts) - (cl-incf n) - (let* ((address (plist-get contact :address)) - (address - (if (functionp mu4e-contact-process-function) - (funcall mu4e-contact-process-function address) - address))) - (when address ;; note the explicit deccode; the strings we get are - ;; utf-8, but emacs doesn't know yet. - (puthash (decode-coding-string address 'utf-8) - (plist-get contact :rank) mu4e--contacts-hash)))) - - (setq mu4e--contacts-tstamp (or tstamp "0")) - - (unless (zerop n) - (mu4e-index-message "Contacts updated: %d; total %d" - n (hash-table-count mu4e--contacts-hash))))) - -(defun mu4e-contacts-info () - "Display information about the contacts-cache. -For testing/debugging." - (interactive) - (with-current-buffer (get-buffer-create "*mu4e-contacts-info*") - (erase-buffer) - (insert (format "complete addresses: %s\n" - (if mu4e-compose-complete-addresses "yes" "no"))) - (insert (format "only personal addresses: %s\n" - (if mu4e-compose-complete-only-personal "yes" "no"))) - (insert (format "only addresses seen after: %s\n" - (or mu4e-compose-complete-only-after "no restrictions"))) - - (when mu4e--contacts-hash - (insert (format "number of contacts cached: %d\n\n" - (hash-table-count mu4e--contacts-hash))) - (let ((contacts)) - (maphash (lambda (addr rank) - (setq contacts (cons (cons rank addr) contacts))) - mu4e--contacts-hash) - (setq contacts (sort contacts - (lambda(cell1 cell2) (< (car cell1) (car cell2))))) - (dolist (contact contacts) - (insert (format "%s\n" (cdr contact)))))) - - (pop-to-buffer "*mu4e-contacts-info*"))) - -(declare-function mu4e--server-contacts "mu4e-server") - -(defun mu4e--request-contacts-maybe () - "If `mu4e-compose-complete-addresses' is non-nil, get/update -the list of contacts we use for autocompletion; otherwise, do -nothing." - (when mu4e-compose-complete-addresses - (mu4e--server-contacts - mu4e-compose-complete-only-personal - mu4e-compose-complete-only-after - mu4e--contacts-tstamp))) - -(provide 'mu4e-contacts) -;;; mu4e-contacts.el ends here diff --git a/mu4e/mu4e-context.el b/mu4e/mu4e-context.el index 90eb0d56..afecb4ac 100644 --- a/mu4e/mu4e-context.el +++ b/mu4e/mu4e-context.el @@ -27,66 +27,38 @@ ;;; Code: -(require 'mu4e-helpers) +(require 'cl-lib) +(require 'mu4e-utils) - -;;; Configuration -(defcustom mu4e-context-policy 'ask-if-none - "The policy to determine the context when entering the mu4e main view. +(defvar smtpmail-smtp-user) +(defvar mu4e-view-date-format) -If the value is `always-ask', ask the user unconditionally. - -In all other cases, if any context matches (using its match -function), this context is used. Otherwise, if none of the -contexts match, we have the following choices: - -- `pick-first': pick the first of the contexts available (ie. the default) -- `ask': ask the user -- `ask-if-none': ask if there is no context yet, otherwise leave it as it is -- nil: return nil; leaves the current context as is. - -Also see `mu4e-compose-context-policy'." - :type '(choice - (const :tag "Always ask what context to use, even if one matches" - always-ask) - (const :tag "Ask if none of the contexts match" ask) - (const :tag "Ask when there's no context yet" ask-if-none) - (const :tag "Pick the first context if none match" pick-first) - (const :tag "Don't change the context when none match" nil)) - :group 'mu4e) - - -(defvar mu4e-contexts nil - "The list of `mu4e-context' objects describing mu4e's contexts.") +(defvar mu4e-contexts nil "The list of `mu4e-context' objects +describing mu4e's contexts.") (defvar mu4e-context-changed-hook nil "Hook run just *after* the context changed.") -(defface mu4e-context-face - '((t :inherit mu4e-title-face :weight bold)) - "Face for displaying the context in the modeline." - :group 'mu4e-faces) +(defvar mu4e~context-current nil + "The current context; for internal use. Use + `mu4e-context-switch' to change it.") -(defvar mu4e--context-current nil - "The current context. -Internal; 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)) + (let ((ctx mu4e~context-current)) (when output (mu4e-message "Current context: %s" (if ctx (mu4e-context-name ctx) ""))) ctx)) (defun mu4e-context-label () - "Propertized string with the current context name. -An empty string \"\" if there is none." + "Propertized string with the current context name, or \"\" if + there is none." (if (mu4e-context-current) - (concat "[" (propertize (mu4e-quote-for-modeline + (concat "[" (propertize (mu4e~quote-for-modeline (mu4e-context-name (mu4e-context-current))) 'face 'mu4e-context-face) "]") "")) @@ -111,8 +83,8 @@ An empty string \"\" if there is none." vars) ;; alist of variables. -(defun mu4e--context-ask-user (prompt) - "Let user choose some context based on its name with PROMPT." +(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)) @@ -121,8 +93,8 @@ An empty string \"\" if there is none." (or context (mu4e-error "No such context"))))) (defun mu4e-context-switch (&optional force name) - "Switch to a context with NAME. -Context must be part of `mu4e-contexts'; if NAME is nil, query user. + "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 @@ -136,13 +108,13 @@ non-nil." (context (if name (cdr-safe (assoc name names)) - (mu4e--context-ask-user "Switch to context: ")))) + (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))) + (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))) @@ -150,30 +122,26 @@ non-nil." (mapc (lambda (cell) (set (car cell) (cdr cell))) (mu4e-context-vars context))) - (setq mu4e--context-current context) + (setq mu4e~context-current context) (run-hooks 'mu4e-context-changed-hook) (mu4e-message "Switched context to %s" (mu4e-context-name context)) (force-mode-line-update)) context)) -(defun mu4e--context-autoswitch (&optional msg policy) - "Automatically switch to some context. - -When contexts are defined but there is no context yet, switch to -the first whose :match-func return non-nil. If none of them -match, return the first. For MSG and POLICY, see -`mu4e-context-determine'." +(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 where match-func evaluate to non-nil. - -MSG points to the plist for the message replied to or forwarded, -or nil if there is no such MSG; similar to what + "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 @@ -189,7 +157,7 @@ match, POLICY determines what to do: - 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: ") + (mu4e~context-ask-user "Select context: ") (or ;; is there a matching one? (cl-find-if (lambda (context) (when (mu4e-context-match-func context) @@ -198,9 +166,9 @@ match, POLICY determines what to do: ;; no context found yet; consult policy (cl-case policy (pick-first (car mu4e-contexts)) - (ask (mu4e--context-ask-user "Select context: ")) + (ask (mu4e~context-ask-user "Select context: ")) (ask-if-none (or (mu4e-context-current) - (mu4e--context-ask-user "Select context: "))) + (mu4e~context-ask-user "Select context: "))) (otherwise nil)))))) (defun mu4e-context-in-modeline () @@ -210,28 +178,6 @@ global-mode-line." (make-local-variable 'global-mode-string) '(:eval (mu4e-context-label)))) -(defmacro with-mu4e-context-vars (context &rest body) - "Evaluate BODY, with variables let-bound for CONTEXT (if any). -`funcall'." - (declare (indent 2)) - `(let* ((vars (and ,context (mu4e-context-vars ,context)))) - (cl-progv ;; XXX: perhaps use eval's lexical environment instead of progv? - (mapcar (lambda(cell) (car cell)) vars) - (mapcar (lambda(cell) (cdr cell)) vars) - (eval ,@body)))) - -(define-minor-mode mu4e-context-minor-mode - "Mode for switching the mu4e context." - :global nil - :init-value nil ;; disabled by default - :group 'mu4e - :lighter "" - :keymap - (let ((map (make-sparse-keymap))) - (define-key map (kbd";") #'mu4e-context-switch) - map) - (mu4e-context-in-modeline)) - -;;; +;;; _ (provide 'mu4e-context) ;;; mu4e-context.el ends here diff --git a/mu4e/mu4e-contrib.el b/mu4e/mu4e-contrib.el index 767c2c6c..c784373e 100644 --- a/mu4e/mu4e-contrib.el +++ b/mu4e/mu4e-contrib.el @@ -96,7 +96,7 @@ BOOKMARK is a bookmark name or a bookmark record." (docid (cdr path)) (query (car path))) (call-interactively 'mu4e) - (mu4e-search query) + (mu4e-headers-search query) (sit-for 0.5) (mu4e~headers-goto-docid docid) (mu4e~headers-highlight docid) @@ -172,17 +172,6 @@ For example for bogofile, use \"/usr/bin/bogofilter -Sn < %s\"") ;; allowing files to be attached to an email via mu4e using the ;; eshell. Does not depend on gnus. - -(defun mu4e~active-composition-buffers () - "Return all active mu4e composition buffers" - (let (buffers) - (save-excursion - (dolist (buffer (buffer-list t)) - (set-buffer buffer) - (when (eq major-mode 'mu4e-compose-mode) - (push (buffer-name buffer) buffers)))) - (nreverse buffers))) - (defun eshell/mu4e-attach (&rest args) "Attach files to a mu4e message using eshell. If no mu4e buffers found, compose a new message and then attach the file." diff --git a/mu4e/mu4e-draft.el b/mu4e/mu4e-draft.el index 839b0461..d68d3f15 100644 --- a/mu4e/mu4e-draft.el +++ b/mu4e/mu4e-draft.el @@ -27,232 +27,12 @@ ;;; Code: (require 'cl-lib) +(require 'mu4e-vars) +(require 'mu4e-utils) (require 'mu4e-message) -(require 'mu4e-contacts) -(require 'mu4e-folders) (require 'message) ;; mail-header-separator - -;;; Configuration -(defgroup mu4e-compose nil - "Customizations for composing/sending messages." - :group 'mu4e) - -(defcustom mu4e-compose-reply-recipients 'ask - "Which recipients to use when replying to a message. -May be 'ask, 'all, 'sender. Note that that only applies to -non-mailing-list message; for those, mu4e always asks." - :type '(choice ask - all - sender) - :group 'mu4e-compose) - -(defcustom mu4e-compose-reply-to-address nil - "The Reply-To address. -Useful when this is not equal to the From: address." - :type 'string - :group 'mu4e-compose) - -(defcustom mu4e-compose-forward-as-attachment nil - "Whether to forward messages as attachments instead of inline." - :type 'boolean - :group 'mu4e-compose) - -;; backward compatibility -(make-obsolete-variable 'mu4e-reply-to-address - 'mu4e-compose-reply-to-address - "v0.9.9") - -(defcustom mu4e-compose-keep-self-cc nil - "When non-nil. keep your e-mail address in Cc: when replying." - :type 'boolean - :group 'mu4e-compose) - -(defvar mu4e-compose-parent-message nil - "The parent message plist. -This is the message being replied to, forwarded or edited; used -in `mu4e-compose-pre-hook'. For new messages, it is nil.") - -(make-obsolete-variable 'mu4e-auto-retrieve-keys "no longer used." "1.3.1") - -(defcustom mu4e-decryption-policy t - "Policy for dealing with replying/forwarding encrypted parts. -The setting is a symbol: - * t: try to decrypt automatically - * `ask': ask before decrypting anything - * nil: don't try to decrypt anything." - :type '(choice (const :tag "Try to decrypt automatically" t) - (const :tag "Ask before decrypting anything" ask) - (const :tag "Don't try to decrypt anything" nil)) - :group 'mu4e-compose) - -;;; Composing / Sending messages - -(defcustom mu4e-sent-messages-behavior 'sent - "Determines what mu4e does with sent messages. - -This is one of the symbols: -* `sent' move the sent message to the Sent-folder (`mu4e-sent-folder') -* `trash' move the sent message to the Trash-folder (`mu4e-trash-folder') -* `delete' delete the sent message. - -Note, when using GMail/IMAP, you should set this to either -`trash' or `delete', since GMail already takes care of keeping -copies in the sent folder. - -Alternatively, `mu4e-sent-messages-behavior' can be a function -which takes no arguments, and which should return one of the mentioned -symbols, for example: - - (setq mu4e-sent-messages-behavior (lambda () - (if (string= (message-sendmail-envelope-from) \"foo@example.com\") - 'delete 'sent))) - -The various `message-' functions from `message-mode' are available -for querying the message information." - :type '(choice (const :tag "move message to mu4e-sent-folder" sent) - (const :tag "move message to mu4e-trash-folder" trash) - (const :tag "delete message" delete)) - :group 'mu4e-compose) - -(defcustom mu4e-compose-context-policy 'ask - "Policy for determining the context when composing a new message. - -If the value is `always-ask', ask the user unconditionally. - -In all other cases, if any context matches (using its match -function), this context is used. Otherwise, if none of the -contexts match, we have the following choices: - -- `pick-first': pick the first of the contexts available (ie. the default) -- `ask': ask the user -- `ask-if-none': ask if there is no context yet, otherwise leave it as it is -- nil: return nil; leaves the current context as is. - -Also see `mu4e-context-policy'." - :type '(choice - (const :tag "Always ask what context to use" always-ask) - (const :tag "Ask if none of the contexts match" ask) - (const :tag "Ask when there's no context yet" ask-if-none) - (const :tag "Pick the first context if none match" pick-first) - (const :tag "Don't change the context when none match" nil)) - :safe 'symbolp - :group 'mu4e-compose) - -(defcustom mu4e-compose-crypto-policy - '(encrypt-encrypted-replies sign-encrypted-replies) - "Policy to control when messages will be signed/encrypted. - -The value is a list, whose members determine the behaviour of -`mu4e~compose-crypto-message'. Specifically, it might contain: - -- `sign-all-messages': Always add a signature. -- `sign-new-messages': Add a signature to new message, ie. - messages that aren't responses to another message. -- `sign-forwarded-messages': Add a signature when forwarding - a message -- `sign-edited-messages': Add a signature to drafts -- `sign-all-replies': Add a signature when responding to - another message. -- `sign-plain-replies': Add a signature when responding to - non-encrypted messages. -- `sign-encrypted-replies': Add a signature when responding - to encrypted messages. - -It should be noted that certain symbols have priorities over one -another. So `sign-all-messages' implies `sign-all-replies', which -in turn implies `sign-plain-replies'. Adding both to the set, is -not a contradiction, but a redundant configuration. - -All `sign-*' options have a `encrypt-*' analogue." - :type '(set :greedy t - (const :tag "Sign all messages" sign-all-messages) - (const :tag "Encrypt all messages" encrypt-all-messages) - (const :tag "Sign new messages" sign-new-messages) - (const :tag "Encrypt new messages" encrypt-new-messages) - (const :tag "Sign forwarded messages" sign-forwarded-messages) - (const :tag "Encrypt forwarded messages" encrypt-forwarded-messages) - (const :tag "Sign edited messages" sign-edited-messages) - (const :tag "Encrypt edited messages" edited-forwarded-messages) - (const :tag "Sign all replies" sign-all-replies) - (const :tag "Encrypt all replies" encrypt-all-replies) - (const :tag "Sign replies to plain messages" sign-plain-replies) - (const :tag "Encrypt replies to plain messages" encrypt-plain-replies) - (const :tag "Sign replies to encrypted messages" sign-encrypted-replies) - (const :tag "Encrypt replies to encrypted messages" encrypt-encrypted-replies)) - :group 'mu4e-compose) - -(defcustom mu4e-compose-crypto-reply-encrypted-policy nil - "Policy for signing/encrypting replies to encrypted messages. -We have the following choices: - -- `sign': sign the reply -- `sign-and-encrypt': sign and encrypt the reply -- `encrypt': encrypt the reply, but don't sign it. -- anything else: do nothing." - :type '(choice - (const :tag "Sign the reply" sign) - (const :tag "Sign and encrypt the reply" sign-and-encrypt) - (const :tag "Encrypt the reply" encrypt) - (const :tag "Don't do anything" nil)) - :safe 'symbolp - :group 'mu4e-compose) - -(make-obsolete-variable 'mu4e-compose-crypto-reply-encrypted-policy "The use of the - 'mu4e-compose-crypto-reply-encrypted-policy' variable is deprecated. - 'mu4e-compose-crypto-policy' should be used instead" - "2020-03-06") - -(defcustom mu4e-compose-crypto-reply-plain-policy nil - "Policy for signing/encrypting replies to messages received unencrypted. -We have the following choices: - -- `sign': sign the reply -- `sign-and-encrypt': sign and encrypt the reply -- `encrypt': encrypt the reply, but don't sign it. -- anything else: do nothing." - :type '(choice - (const :tag "Sign the reply" sign) - (const :tag "Sign and encrypt the reply" sign-and-encrypt) - (const :tag "Encrypt the reply" encrypt) - (const :tag "Don't do anything" nil)) - :safe 'symbolp - :group 'mu4e-compose) - -(make-obsolete-variable 'mu4e-compose-crypto-reply-plain-policy "The use of the - 'mu4e-compose-crypto-reply-plain-policy' variable is deprecated. - 'mu4e-compose-crypto-policy' should be used instead" - "2020-03-06") - -(make-obsolete-variable 'mu4e-compose-crypto-reply-policy "The use of the - 'mu4e-compose-crypto-reply-policy' variable is deprecated. - 'mu4e-compose-crypto-reply-plain-policy' and - 'mu4e-compose-crypto-reply-encrypted-policy' should be used instead" - "2017-09-02") - -(defcustom mu4e-compose-format-flowed nil - "Whether to compose messages to be sent as format=flowed. -\(Or with long lines if variable `use-hard-newlines' is set to -nil). The variable `fill-flowed-encode-column' lets you customize -the width beyond which format=flowed lines are wrapped." - :type 'boolean - :safe 'booleanp - :group 'mu4e-compose) - -(defcustom mu4e-compose-pre-hook nil - "Hook run just *before* message composition starts. -If the compose-type is either 'reply' or 'forward', the variable -`mu4e-compose-parent-message' points to the message replied to / -being forwarded / edited, and `mu4e-compose-type' contains the -type of message to be composed. - -Note that there is no draft message yet when this hook runs, it -is meant for influencing the how mu4e constructs the draft -message. If you want to do something with the draft messages after -it has been constructed, `mu4e-compose-mode-hook' would be the -place to do that." - :type 'hook - :group 'mu4e-compose) +;;; Options (defcustom mu4e-compose-dont-reply-to-self nil "If non-nil, don't include self. @@ -299,11 +79,6 @@ mu4e-specific version of `message-signature'." (defvar mu4e-view-date-format) -(defvar mu4e-compose-type nil - "The compose-type for this buffer. -This is a symbol, `new', `forward', `reply' or `edit'.") - - (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 @@ -344,22 +119,6 @@ one. Code borrowed from `message-shorten-1'." (setcdr (nthcdr (- cut 2) list) (nthcdr (+ (- cut 2) surplus 1) list))) - - -(defun mu4e~fontify-signature () - "Give the message signatures a distinctive color. This is used in -the view and compose modes and will color each signature in digest messages adhering to RFC 1153." - (let ((inhibit-read-only t)) - (save-excursion - ;; give the footer a different color... - (goto-char (point-min)) - (while (re-search-forward "^-- *$" nil t) - (let ((p (point)) - (end (or - (re-search-forward "\\(^-\\{30\\}.*$\\)" nil t) ;; 30 by RFC1153 - (point-max)))) - (add-text-properties p end '(face mu4e-footer-face))))))) - (defun mu4e~draft-references-construct (msg) "Construct the value of the References: header based on MSG. This assumes a comma-separated string. Normally, this the concatenation of the @@ -799,13 +558,13 @@ will be created from either `mu4e~draft-reply-construct', or ;; 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))) + (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))) + (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))) diff --git a/mu4e/mu4e-folders.el b/mu4e/mu4e-folders.el deleted file mode 100644 index a2780d02..00000000 --- a/mu4e/mu4e-folders.el +++ /dev/null @@ -1,354 +0,0 @@ -;;; mu4e-folders.el -- part of mu4e -*- lexical-binding: t -*- - -;; Copyright (C) 2021 Dirk-Jan C. Binnema - -;; Author: Dirk-Jan C. Binnema -;; Maintainer: 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 . - -;;; Commentary: - -;; Dealing with maildirs & folders - -;;; Code: -(require 'cl-lib) -(require 'mu4e-helpers) -(require 'mu4e-context) -(require 'mu4e-server) - -;;; Customization -(defgroup mu4e-folders nil - "Special folders." - :group 'mu4e) - -(defcustom mu4e-drafts-folder "/drafts" - "Folder for draft messages, relative to the root maildir. -For instance, \"/drafts\". Instead of a string, may also be a -function that takes a message (a msg plist, see -`mu4e-message-field'), and returns a folder. Note, the message -parameter refers to the original message being replied to / being -forwarded / re-edited and is nil otherwise. `mu4e-drafts-folder' -is only evaluated once." - :type '(choice - (string :tag "Folder name") - (function :tag "Function return folder name")) - :group 'mu4e-folders) - -(defcustom mu4e-refile-folder "/archive" - "Folder for refiling messages, relative to the root maildir. -For instance \"/Archive\". Instead of a string, may also be a -function that takes a message (a msg plist, see -`mu4e-message-field'), and returns a folder. Note that the -message parameter refers to the message-at-point." - :type '(choice - (string :tag "Folder name") - (function :tag "Function return folder name")) - :group 'mu4e-folders) - -(defcustom mu4e-sent-folder "/sent" - "Folder for sent messages, relative to the root maildir. -For instance, \"/Sent Items\". Instead of a string, may also be a -function that takes a message (a msg plist, see -`mu4e-message-field'), and returns a folder. Note that the -message parameter refers to the original message being replied to -/ being forwarded / re-edited, and is nil otherwise." - :type '(choice - (string :tag "Folder name") - (function :tag "Function return folder name")) - :group 'mu4e-folders) - -(defcustom mu4e-trash-folder "/trash" - "Folder for trashed messages, relative to the root maildir. -For instance, \"/trash\". Instead of a string, may also be a -function that takes a message (a msg plist, see -`mu4e-message-field'), and returns a folder. When using -`mu4e-trash-folder' in the headers view (when marking messages -for trash). Note that the message parameter refers to the -message-at-point. When using it when composing a message (see -`mu4e-sent-messages-behavior'), this refers to the original -message being replied to / being forwarded / re-edited, and is -nil otherwise." - :type '(choice - (string :tag "Folder name") - (function :tag "Function return folder name")) - :group 'mu4e-folders) - -(defcustom mu4e-maildir-shortcuts nil - "A list of maildir shortcuts. -This makes it possible to quickly go to a particular -maildir (folder), or quickly moving messages to them (e.g., for -archiving or refiling). - -Each of the list elements is a plist with at least: -`:maildir' - the maildir for the shortcut (e.g. \"/archive\") -`:key' - the shortcut key. - -Optionally, you can add the following: -`:hide' - if t, the shortcut is hidden from the main-view and -speedbar. -`:hide-unread' - do not show the counts of unread/total number - of matches for the maildir in the main-view, and is implied -from `:hide'. - -For backward compatibility, an older form is recognized as well: - - (maildir . key), where MAILDIR is a maildir (such as -\"/archive/\"), and key is a single character. - -You can use these shortcuts in the headers and view buffers, for -example with `mu4e-mark-for-move-quick' (or 'm', by default) or -`mu4e-jump-to-maildir' (or 'j', by default), followed by the -designated shortcut character for the maildir. - -Unlike in search queries, folder names with spaces in them must -NOT be quoted, since mu4e does this for you." - :type '(repeat (cons (string :tag "Maildir") character)) - :version "1.3.9" - :group 'mu4e-folders) - -(defcustom mu4e-maildir-info-delimiter - (if (member system-type '(ms-dos windows-nt cygwin)) - ";" ":") - "Separator character between message identifier and flags. -It defaults to ':' on most platforms, except on Windows, where it -is not allowed and we use ';' for compatibility with mbsync, -offlineimap and other programs." - :type 'string - :group 'mu4e-folders) - -(defcustom mu4e-attachment-dir (expand-file-name "~/") - "Default directory for attaching and saving attachments. - -This can be either a string (a file system path), or a function -that takes a filename and the mime-type as arguments, and returns -the attachment dir. See Info node `(mu4e) Attachments' for -details. - -When this called for composing a message, both filename and -mime-type are nill." - :type 'directory - :group 'mu4e-folders - :safe 'stringp) - - - -(defun mu4e-maildir-shortcuts () - "Get `mu4e-maildir-shortcuts' in the (new) format. -Converts from the old format if needed." - (cl-map 'list - (lambda (item) ;; convert from old format? - (if (and (consp item) (not (consp (cdr item)))) - `(:maildir ,(car item) :key ,(cdr item)) - item)) - mu4e-maildir-shortcuts)) - -(defun mu4e--maildirs-with-query () - "Llike `mu4e-maildir-shortcuts', but with :query populated. - -This is meant to be the exact same data structure as -`mu4e-bookmarks'." - (cl-mapcar - (lambda (m) - (append - ;; we want to change the :maildir key to :name, and add a :query key - (list :name (plist-get m :maildir) - :query (format "maildir:\"%s\"" (plist-get m :maildir))) - ;; next we want to append any other keys to our previous list (e.g. :hide, - ;; :key, etc) but skipping :maildir (since it's renamed to :name) - (cl-loop for (key value) on m by 'cddr - when (not (equal key :maildir)) - append (list key value)))) - (mu4e-maildir-shortcuts))) - - -;; the standard folders can be functions too -(defun mu4e--get-folder (foldervar msg) - "Within the mu-context of MSG, get message folder FOLDERVAR. -If FOLDER is a string, return it, if it is a function, evaluate -this function with MSG as parameter which may be nil, and return -the result." - (unless (member foldervar - '(mu4e-sent-folder mu4e-drafts-folder - mu4e-trash-folder mu4e-refile-folder)) - (mu4e-error "Folder must be one of mu4e-(sent|drafts|trash|refile)-folder")) - ;; get the value with the vars for the relevants context let-bound - (with-mu4e-context-vars (mu4e-context-determine msg nil) - (let* ((folder (symbol-value foldervar)) - (val - (cond - ((stringp folder) folder) - ((functionp folder) (funcall folder msg)) - (t (mu4e-error "Unsupported type for %S" folder))))) - (or val (mu4e-error "%S evaluates to nil" foldervar))))) - -(defun mu4e-get-drafts-folder (&optional msg) - "Get the sent folder, optionallly based on MSG. -See `mu4e-drafts-folder'." (mu4e--get-folder 'mu4e-drafts-folder msg)) - -(defun mu4e-get-refile-folder (&optional msg) - "Get the folder for refiling, optionallly based on MSG. -See `mu4e-refile-folder'." (mu4e--get-folder 'mu4e-refile-folder msg)) - -(defun mu4e-get-sent-folder (&optional msg) - "Get the sent folder, optionallly based on MSG. -See `mu4e-sent-folder'." (mu4e--get-folder 'mu4e-sent-folder msg)) - -(defun mu4e-get-trash-folder (&optional msg) - "Get the sent folder, optionallly based on MSG. -See `mu4e-trash-folder'." (mu4e--get-folder 'mu4e-trash-folder msg)) - - -;;; Maildirs -(defun mu4e--guess-maildir (path) - "Guess the maildir for PATH, or nil if cannot find it." - (let ((idx (string-match (mu4e-root-maildir) path))) - (when (and idx (zerop idx)) - (replace-regexp-in-string - (mu4e-root-maildir) - "" - (expand-file-name - (concat path "/../..")))))) - -(defun mu4e-create-maildir-maybe (dir) - "Offer to create maildir DIR if it does not exist yet. -Return t if the dir already existed, or an attempt has been made to -create it -- we cannot be sure creation succeeded here, since this -is done asynchronously. Otherwise, return nil. NOte, DIR has to be -an absolute path." - (if (and (file-exists-p dir) (not (file-directory-p dir))) - (mu4e-error "File %s exists, but is not a directory" dir)) - (cond - ((file-directory-p dir) t) - ((yes-or-no-p (mu4e-format "%s does not exist yet. Create now?" dir)) - (mu4e--server-mkdir dir) t) - (t nil))) - -(defun mu4e~get-maildirs-1 (path mdir) - "Get maildirs for MDIR under PATH. -Do so recursively and produce a list of relative paths." - (let ((dirs) - (dentries - (ignore-errors - (directory-files-and-attributes - (concat path mdir) nil - "^[^.]\\|\\.[^.][^.]" t)))) - (dolist (dentry dentries) - (when (and (booleanp (cadr dentry)) (cadr dentry)) - (if (file-accessible-directory-p - (concat (mu4e-root-maildir) "/" mdir "/" (car dentry) "/cur")) - (setq dirs (cons (concat mdir (car dentry)) dirs))) - (unless (member (car dentry) '("cur" "new" "tmp")) - (setq dirs - (append dirs - (mu4e~get-maildirs-1 path - (concat mdir - (car dentry) "/"))))))) - dirs)) - -(defvar mu4e-cache-maildir-list nil - "Whether to cache the list of maildirs. -Set it to t if you find -that generating the list on the fly is too slow. If you do, you -can set `(mu4e-root-maildir)-list' to nil to force regenerating the -cache the next time `mu4e-get-maildirs' gets called.") - -(defvar mu4e-maildir-list nil - "Cached list of maildirs.") - -(defun mu4e-get-maildirs () - "Get maildirs under `mu4e-maildir'. -Do so recursively, and produce a list of relative paths (ie., -/archive, /sent etc.). Most of the work is done in -`mu4e~get-maildirs-1'. Note, these results are /cached/ if -`mu4e-cache-maildir-list' is customized to non-nil. In that case, -the list of maildirs will not change until you restart mu4e." - (unless (and mu4e-maildir-list mu4e-cache-maildir-list) - (setq mu4e-maildir-list - (sort - (append - (when (file-accessible-directory-p - (concat (mu4e-root-maildir) "/cur")) '("/")) - (mu4e~get-maildirs-1 (mu4e-root-maildir) "/")) - (lambda (s1 s2) (string< (downcase s1) (downcase s2)))))) - mu4e-maildir-list) - -(defun mu4e-ask-maildir (prompt) - "Ask the user for a shortcut (using PROMPT). -As per (mu4e-maildir-shortcuts), then return the corresponding folder -name. If the special shortcut 'o' (for _o_ther) is used, or if -`(mu4e-maildir-shortcuts)' evaluates to nil, let user choose from -all maildirs under `mu4e-maildir'." - (let ((prompt (mu4e-format "%s" prompt))) - (if (not (mu4e-maildir-shortcuts)) - (substring-no-properties - (funcall mu4e-completing-read-function prompt (mu4e-get-maildirs))) - (let* ((mlist (append (mu4e-maildir-shortcuts) - '((:maildir "ther" :key ?o)))) - (fnames - (mapconcat - (lambda (item) - (concat - "[" - (propertize (make-string 1 (plist-get item :key)) - 'face 'mu4e-highlight-face) - "]" - (plist-get item :maildir))) - mlist ", ")) - (kar (read-char (concat prompt fnames)))) - (if (member kar '(?/ ?o)) ;; user chose 'other'? - (substring-no-properties - (funcall mu4e-completing-read-function prompt - (mu4e-get-maildirs) nil nil "/")) - (or (plist-get - (cl-find-if (lambda (item) (= kar (plist-get item :key))) - (mu4e-maildir-shortcuts)) :maildir) - (mu4e-warn "Unknown shortcut '%c'" kar))))))) - -(defun mu4e-ask-maildir-check-exists (prompt) - "Like `mu4e-ask-maildir', PROMPT for existence of the maildir. -Offer to create it if it does not exist yet." - (let* ((mdir (mu4e-ask-maildir prompt)) - (fullpath (concat (mu4e-root-maildir) mdir))) - (unless (file-directory-p fullpath) - (and (yes-or-no-p - (mu4e-format "%s does not exist. Create now?" fullpath)) - (mu4e--server-mkdir fullpath))) - mdir)) - -;; mu4e-attachment-dir is either a string or a function that takes a -;; filename and the mime-type as argument, either (or both) which can -;; be nil - -(defun mu4e~get-attachment-dir (&optional fname mimetype) - "Get the directory for saving attachments from -`mu4e-attachment-dir' (which can be either a string or a function, -see its docstring)." - (let - ((dir - (cond - ((stringp mu4e-attachment-dir) - mu4e-attachment-dir) - ((functionp mu4e-attachment-dir) - (funcall mu4e-attachment-dir fname mimetype)) - (t - (mu4e-error "unsupported type for mu4e-attachment-dir" ))))) - (if dir - (expand-file-name dir) - (mu4e-error "mu4e-attachment-dir evaluates to nil")))) - -(provide 'mu4e-folders) -;;; mu4e-folders.el ends here diff --git a/mu4e/mu4e-headers.el b/mu4e/mu4e-headers.el index d6afb0e8..203de0c0 100644 --- a/mu4e/mu4e-headers.el +++ b/mu4e/mu4e-headers.el @@ -34,25 +34,18 @@ (require 'mailcap) (require 'mule-util) ;; seems _some_ people need this for truncate-string-ellipsis -(require 'mu4e-update) - (require 'mu4e-utils) ;; utility functions -(require 'mu4e-server) +(require 'mu4e-proc) (require 'mu4e-vars) (require 'mu4e-mark) -(require 'mu4e-context) -(require 'mu4e-search) (require 'mu4e-compose) (require 'mu4e-actions) (require 'mu4e-message) -(require 'mu4e-folders) (declare-function mu4e-view "mu4e-view") (declare-function mu4e~main-view "mu4e-main") - - -;;; Configuration +;;; Options (defgroup mu4e-headers nil "Settings for the headers view." @@ -127,6 +120,27 @@ indexing operation showed changes." :type 'boolean :group 'mu4e-headers) +(defcustom mu4e-headers-results-limit 500 + "Maximum number of results to show; this affects performance +quite a bit, especially when `mu4e-headers-include-related' is +non-nil. Set to -1 for no limits, and you temporarily (for one +query) ignore the limit by pressing a C-u before invoking the +search. + +Note that there are a few complications when +`mu4e-headers-include-related' is enabled: mu performs *two* +queries; the first one with this limit set, and then a second +(unlimited) query for all messages that are related to the first +matches. We then limit this second result as well, favoring the +messages that were found in the first set (the \"leaders\"). +" + :type '(choice (const :tag "Unlimited" -1) + (integer :tag "Limit")) + :group 'mu4e-headers) + +(make-obsolete-variable 'mu4e-search-results-limit + 'mu4e-headers-results-limit "0.9.9.5-dev6") + (defcustom mu4e-headers-advance-after-mark t "With this option set to non-nil, automatically advance to the next mail after marking a message in header view." @@ -172,6 +186,33 @@ query have been received and are displayed." :type 'hook :group 'mu4e-headers) +(defcustom mu4e-headers-search-bookmark-hook nil + "Hook run just after we invoke a bookmarked search. This +function receives the query as its parameter, before any +rewriting as per `mu4e-query-rewrite-function' has taken place. + +The reason to use this instead of `mu4e-headers-search-hook' is +if you only want to execute a hook when a search is entered via a +bookmark, e.g. if you'd like to treat the bookmarks as a custom +folder and change the options for the search, e.g. +`mu4e-headers-show-threads', `mu4e-headers-include-related', +`mu4e-headers-skip-duplicates` or `mu4e-headers-results-limit'. +" + :type 'hook + :group 'mu4e-headers) + +(defcustom mu4e-headers-search-hook nil + "Hook run just before executing a new search operation. This +function receives the query as its parameter, before any +rewriting as per `mu4e-query-rewrite-function' has taken place + +This is a more general hook facility than the +`mu4e-headers-search-bookmark-hook'. It gets called on every +executed search, not just those that are invoked via bookmarks, +but also manually invoked searches." + :type 'hook + :group 'mu4e-headers) + ;;; Public variables (defvar mu4e-headers-sort-field :date @@ -180,7 +221,7 @@ one of: `:date', `:subject', `:size', `:prio', `:from', `:to.', `:list'. Note that when threading is enabled (through -`mu4e-search-threads'), the headers are exclusively sorted +`mu4e-headers-show-threads'), the headers are exclusively sorted chronologically (`:date') by the newest message in the thread.") (defvar mu4e-headers-sort-direction 'descending @@ -267,6 +308,14 @@ and (optionally) PARAM, and should return non-nil when there's a match. * PARAM-FUNC is function that is evaluated once, and its value is then passed to PREDICATE-FUNC as PARAM. This is useful for getting user-input.") + +(defvar mu4e-headers-show-threads t + "Whether to show threads in the headers list.") + +(defvar mu4e-headers-full-search nil + "Whether to show all results. +If this is nil show results up to `mu4e-headers-results-limit')") + ;;; Internal variables/constants ;; docid cookies @@ -289,7 +338,6 @@ followed by the docid, followed by `mu4e~headers-docid-post'.") "List of cells describing the various sort-options. In the format needed for `mu4e-read-option'.") - ;;; Clear (defvar mu4e~headers-render-start nil) @@ -299,11 +347,6 @@ In the format needed for `mu4e-read-option'.") "If non-nil, report on the time it took to render the messages. This is mostly useful for profiling.") - - - - - (defun mu4e~headers-clear (&optional msg) "Clear the header buffer and related data structures." (when (buffer-live-p (mu4e-get-headers-buffer)) @@ -751,48 +794,10 @@ if provided, or at the end of the buffer otherwise." (mu4e~headers-add-header line (mu4e-message-field msg :docid) point msg)))))) - - - -;;; Performing queries (internal) (defconst mu4e~search-message "Searching...") (defconst mu4e~no-matches "No matching messages found") (defconst mu4e~end-of-results "End of search results") - -(defun mu4e--search-execute (expr ignore-history) - "Search for query EXPR. - -Switch to the output buffer for the results. If IGNORE-HISTORY is -true, do *not* update the query history stack." - (let* ((buf (get-buffer-create mu4e-headers-buffer-name)) - (inhibit-read-only t) - (rewritten-expr (funcall mu4e-search-query-rewrite-function expr)) - (maxnum (unless mu4e-search-full mu4e-search-results-limit))) - (with-current-buffer buf - (mu4e-headers-mode) - (unless ignore-history - ;; save the old present query to the history list - (when mu4e--search-last-query - (mu4e--search-push-query mu4e--search-last-query 'past))) - (setq mu4e--search-last-query rewritten-expr) - (mu4e~headers-update-mode-line)) - - ;; when the buffer is already visible, select it; otherwise, - ;; switch to it. - (unless (get-buffer-window buf 0) - (switch-to-buffer buf)) - (run-hook-with-args 'mu4e-search-hook expr) - (mu4e~headers-clear mu4e~search-message) - (mu4e--server-find - rewritten-expr - mu4e-search-threads - mu4e-headers-sort-field - mu4e-headers-sort-direction - maxnum - mu4e-headers-skip-duplicates - mu4e-headers-include-related))) - (defvar mu4e~headers-view-target nil "Whether to automatically view (open) the target message (as per `mu4e~headers-msgid-target').") @@ -878,8 +883,24 @@ after the end of the search results." (setq mu4e-headers-mode-map (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-S-u") 'mu4e-update-mail-and-index) + ;; for terminal users + (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index) + + (define-key map "s" 'mu4e-headers-search) + (define-key map "S" 'mu4e-headers-search-edit) + + (define-key map "/" 'mu4e-headers-search-narrow) + (define-key map "j" 'mu4e~headers-jump-to-maildir) + (define-key map (kbd "") 'mu4e-headers-query-prev) + (define-key map (kbd "\\") 'mu4e-headers-query-prev) + (define-key map (kbd "") 'mu4e-headers-query-next) + + (define-key map "b" 'mu4e-headers-search-bookmark) + (define-key map "B" 'mu4e-headers-search-bookmark-edit) + (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) @@ -908,6 +929,8 @@ after the end of the search results." (define-key map (kbd "") 'mu4e-headers-split-view-grow) (define-key map (kbd "") 'mu4e-headers-split-view-shrink) + (define-key map ";" 'mu4e-context-switch) + ;; switching to view mode (if it's visible) (define-key map "y" 'mu4e-select-other-view) @@ -976,8 +999,8 @@ after the end of the search results." (define-key menumap [toggle-threading] '(menu-item "Toggle threading" mu4e-headers-toggle-threading :button (:toggle . - (and (boundp 'mu4e-search-threads) - mu4e-search-threads)))) + (and (boundp 'mu4e-headers-show-threads) + mu4e-headers-show-threads)))) (define-key menumap "|" '("Pipe through shell" . mu4e-view-pipe)) (define-key menumap [sepa1] '("--")) @@ -1044,7 +1067,7 @@ after the end of the search results." (mapcar (lambda (item) (let* ( ;; with threading enabled, we're necessarily sorting by date. - (sort-field (if mu4e-search-threads :date mu4e-headers-sort-field)) + (sort-field (if mu4e-headers-show-threads :date mu4e-headers-sort-field)) (field (car item)) (width (cdr item)) (info (cdr (assoc field (append mu4e-header-info mu4e-header-info-custom)))) @@ -1099,7 +1122,7 @@ no user-interaction ongoing." ;; otherwise we'd trigger a headers view from out of nowhere. (when (and (buffer-live-p (mu4e-get-headers-buffer)) (window-live-p (get-buffer-window (mu4e-get-headers-buffer) t))) - (mu4e-search-rerun)))) + (mu4e-headers-rerun-search)))) (define-derived-mode mu4e-headers-mode special-mode "mu4e:headers" @@ -1110,6 +1133,8 @@ no user-interaction ongoing." (make-local-variable 'mu4e~highlighted-docid) (set (make-local-variable 'hl-line-face) 'mu4e-header-highlight-face) + (mu4e-context-in-modeline) + ;; maybe update the current headers upon indexing changes (add-hook 'mu4e-index-updated-hook 'mu4e~headers-maybe-auto-update) (add-hook 'mu4e-index-updated-hook @@ -1122,9 +1147,6 @@ no user-interaction ongoing." header-line-format (mu4e~header-line-format)) (mu4e~mark-initialize) ;; initialize the marking subsystem - (mu4e-context-minor-mode) - (mu4e-update-minor-mode) - (mu4e-search-minor-mode) (hl-line-mode 1)) (defun mu4e~headers-index-updated-hook-fn () @@ -1229,14 +1251,14 @@ docid is not found." (if mu4e-use-fancy-chars (cddr flag-cell) (cadr flag-cell) ) "")) - `((,mu4e-search-full . ,mu4e-headers-full-label) + `((,mu4e-headers-full-search . ,mu4e-headers-full-label) (,mu4e-headers-include-related . ,mu4e-headers-related-label) - (,mu4e-search-threads . ,mu4e-headers-threaded-label)) + (,mu4e-headers-show-threads . ,mu4e-headers-threaded-label)) "")) (name "mu4e-headers")) (setq mode-name name) - (setq mu4e~headers-mode-line-label (concat flagstr " " mu4e--search-last-query)) + (setq mu4e~headers-mode-line-label (concat flagstr " " mu4e~headers-last-query)) (make-local-variable 'global-mode-string) @@ -1244,7 +1266,7 @@ docid is not found." `(:eval (concat (propertize - (mu4e-quote-for-modeline ,mu4e~headers-mode-line-label) + (mu4e~quote-for-modeline ,mu4e~headers-mode-line-label) 'face 'mu4e-modeline-face) " " (if (and mu4e-display-update-status-in-modeline @@ -1255,6 +1277,41 @@ docid is not found." "")))))) +(defun mu4e~headers-search-execute (expr ignore-history) + "Search in the mu database for EXPR, and switch to the output +buffer for the results. If IGNORE-HISTORY is true, do *not* update +the query history stack." + ;; note: we don't want to update the history if this query comes from + ;; `mu4e~headers-query-next' or `mu4e~headers-query-prev'. + ;;(mu4e-hide-other-mu4e-buffers) + (let* ((buf (get-buffer-create mu4e~headers-buffer-name)) + (inhibit-read-only t) + (rewritten-expr (funcall mu4e-query-rewrite-function expr)) + (maxnum (unless mu4e-headers-full-search mu4e-headers-results-limit))) + (with-current-buffer buf + (mu4e-headers-mode) + (unless ignore-history + ;; save the old present query to the history list + (when mu4e~headers-last-query + (mu4e~headers-push-query mu4e~headers-last-query 'past))) + (setq mu4e~headers-last-query rewritten-expr) + (mu4e~headers-update-mode-line)) + + ;; when the buffer is already visible, select it; otherwise, + ;; switch to it. + (unless (get-buffer-window buf 0) + (switch-to-buffer buf)) + (run-hook-with-args 'mu4e-headers-search-hook expr) + (mu4e~headers-clear mu4e~search-message) + (mu4e~proc-find + rewritten-expr + mu4e-headers-show-threads + mu4e-headers-sort-field + mu4e-headers-sort-direction + maxnum + mu4e-headers-skip-duplicates + mu4e-headers-include-related))) + (defun mu4e~headers-redraw-get-view-window () "Close all windows, redraw the headers buffer based on the value of `mu4e-split-view', and return a window for the message view." @@ -1448,8 +1505,166 @@ descendants." (let ((current-prefix-arg t)) (call-interactively 'mu4e-headers-mark-thread)))) + +;;; The query past / present / future + +(defvar mu4e~headers-query-past nil + "Stack of queries before the present one.") +(defvar mu4e~headers-query-future nil + "Stack of queries after the present one.") +(defvar mu4e~headers-query-stack-size 20 + "Maximum size for the query stacks.") + +(defun mu4e~headers-push-query (query where) + "Push QUERY to one of the query stacks. +WHERE is a symbol telling us where to push; it's a symbol, either +'future or 'past. Functional also removes duplicates, limits the +stack size." + (let ((stack + (cl-case where + (past mu4e~headers-query-past) + (future mu4e~headers-query-future)))) + ;; only add if not the same item + (unless (and stack (string= (car stack) query)) + (push query stack) + ;; limit the stack to `mu4e~headers-query-stack-size' elements + (when (> (length stack) mu4e~headers-query-stack-size) + (setq stack (cl-subseq stack 0 mu4e~headers-query-stack-size))) + ;; remove all duplicates of the new element + (cl-remove-if (lambda (elm) (string= elm (car stack))) (cdr stack)) + ;; update the stacks + (cl-case where + (past (setq mu4e~headers-query-past stack)) + (future (setq mu4e~headers-query-future stack)))))) + +(defun mu4e~headers-pop-query (whence) + "Pop a query from the stack. +WHENCE is a symbol telling us where to get it from, either `future' +or `past'." + (cl-case whence + (past + (unless mu4e~headers-query-past + (mu4e-warn "No more previous queries")) + (pop mu4e~headers-query-past)) + (future + (unless mu4e~headers-query-future + (mu4e-warn "No more next queries")) + (pop mu4e~headers-query-future)))) + + +;;; Reading queries with completion + +(defvar mu4e-minibuffer-search-query-map + (let ((map (copy-keymap minibuffer-local-map))) + (define-key map (kbd "TAB") #'completion-at-point) + map) + + "The keymap when reading a search query.") +(defun mu4e-read-query (prompt &optional initial-input) + "Read a search query with completion using PROMPT and INITIAL-INPUT." + (minibuffer-with-setup-hook + (lambda () + (setq-local completion-at-point-functions + #'mu4e~search-query-competion-at-point) + (use-local-map mu4e-minibuffer-search-query-map)) + (read-string prompt initial-input 'mu4e~headers-search-hist))) + +(defvar mu4e~headers-search-hist nil + "History list of searches.") + +(defconst mu4e~search-query-keywords + '("and" "or" "not" + "from:" "to:" "cc:" "bcc:" "contact:" "date:" "subject:" "body:" + "list:" "maildir:" "flag:" "mime:" "file:" "prio:" "tag:" "msgid:" + "size:" "embed:")) + +(defun mu4e~search-query-competion-at-point () + (cond + ((not (looking-back "[:\"][^ \t]*" nil)) + (let ((bounds (bounds-of-thing-at-point 'word))) + (list (or (car bounds) (point)) + (or (cdr bounds) (point)) + mu4e~search-query-keywords))) + ((looking-back "flag:\\(\\w*\\)" nil) + (list (match-beginning 1) + (match-end 1) + '("attach" "draft" "flagged" "list" "new" "passed" "replied" + "seen" "trashed" "unread" "encrypted" "signed"))) + ((looking-back "maildir:\\([a-zA-Z0-9/.]*\\)" nil) + (list (match-beginning 1) + (match-end 1) + (mu4e-get-maildirs))) + ((looking-back "prio:\\(\\w*\\)" nil) + (list (match-beginning 1) + (match-end 1) + (list "high" "normal" "low"))) + ((looking-back "mime:\\([a-zA-Z0-9/-]*\\)" nil) + (list (match-beginning 1) + (match-end 1) + (mailcap-mime-types))))) + ;;; Interactive functions + +(defun mu4e-headers-search (&optional expr prompt edit + ignore-history msgid show) + "Search in the mu database for EXPR, and switch to the output +buffer for the results. This is an interactive function which ask +user for EXPR. PROMPT, if non-nil, is the prompt used by this +function (default is \"Search for:\"). If EDIT is non-nil, +instead of executing the query for EXPR, let the user edit the +query before executing it. If IGNORE-HISTORY is true, do *not* +update the query history stack. If MSGID is non-nil, attempt to +move point to the first message with that message-id after +searching. If SHOW is non-nil, show the message with MSGID." + ;; note: we don't want to update the history if this query comes from + ;; `mu4e~headers-query-next' or `mu4e~headers-query-prev'." + (interactive) + (let* ((prompt (mu4e-format (or prompt "Search for: "))) + (expr + (if (or (null expr) edit) + (mu4e-read-query prompt expr) + expr))) + (mu4e-mark-handle-when-leaving) + (mu4e~headers-search-execute expr ignore-history) + (setq mu4e~headers-msgid-target msgid + mu4e~headers-view-target show))) + +(defun mu4e-headers-search-edit () + "Edit the last search expression." + (interactive) + (mu4e-headers-search mu4e~headers-last-query nil t)) + +(defun mu4e-headers-search-bookmark (&optional expr edit) + "Search using some bookmarked query EXPR. +If EDIT is non-nil, let the user edit the bookmark before starting +the search." + (interactive) + (let ((expr + (or expr + (mu4e-ask-bookmark (if edit "Select bookmark: " "Bookmark: "))))) + (run-hook-with-args 'mu4e-headers-search-bookmark-hook expr) + (mu4e-headers-search expr (when edit "Edit bookmark: ") edit))) + +(defun mu4e-headers-search-bookmark-edit () + "Edit an existing bookmark before executing it." + (interactive) + (mu4e-headers-search-bookmark nil t)) + +(defun mu4e-headers-search-narrow (filter ) + "Narrow the last search by appending search expression FILTER to +the last search expression. Note that you can go back to previous +query (effectively, 'widen' it), with `mu4e-headers-query-prev'." + (interactive + (let ((filter + (read-string (mu4e-format "Narrow down to: ") + nil 'mu4e~headers-search-hist nil t))) + (list filter))) + (unless mu4e~headers-last-query + (mu4e-warn "There's nothing to filter")) + (mu4e-headers-search + (format "(%s) AND (%s)" mu4e~headers-last-query filter))) + (defun mu4e-headers-change-sorting (&optional field dir) "Change the sorting/threading parameters. FIELD is the field to sort by; DIR is a symbol: either 'ascending, @@ -1485,7 +1700,7 @@ sortfield, change the sort-order) or nil (ask the user)." (mu4e-message "Sorting by %s (%s)" (symbol-name sortfield) (symbol-name mu4e-headers-sort-direction)) - (mu4e-search-rerun))) + (mu4e-headers-rerun-search))) (defun mu4e~headers-toggle (name togglevar dont-refresh) "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil, @@ -1497,20 +1712,20 @@ re-run the last search." (if dont-refresh " (press 'g' to refresh)" "")) (unless dont-refresh - (mu4e-search-rerun))) + (mu4e-headers-rerun-search))) (defun mu4e-headers-toggle-threading (&optional dont-refresh) - "Toggle `mu4e-search-threads'. With prefix-argument, do + "Toggle `mu4e-headers-show-threads'. With prefix-argument, do _not_ refresh the last search with the new setting for threading." (interactive "P") - (mu4e~headers-toggle "Threading" 'mu4e-search-threads dont-refresh)) + (mu4e~headers-toggle "Threading" 'mu4e-headers-show-threads dont-refresh)) (defun mu4e-headers-toggle-full-search (&optional dont-refresh) - "Toggle `mu4e-search-full'. With prefix-argument, do + "Toggle `mu4e-headers-full-search'. With prefix-argument, do _not_ refresh the last search with the new setting for threading." (interactive "P") (mu4e~headers-toggle "Full-search" - 'mu4e-search-full dont-refresh)) + 'mu4e-headers-full-search dont-refresh)) (defun mu4e-headers-toggle-include-related (&optional dont-refresh) "Toggle `mu4e-headers-include-related'. With prefix-argument, do @@ -1529,6 +1744,14 @@ _not_ refresh the last search with the new setting for threading." (defvar mu4e~headers-loading-buf nil "A buffer for loading a message view.") +(defun mu4e~decrypt-p (msg) + "Should we decrypt this message?" + (when mu4e-view-use-old ;; we don't decrypt in the gnus-view case + (and (member 'encrypted (mu4e-message-field msg :flags)) + (if (eq mu4e-decryption-policy 'ask) + (yes-or-no-p (mu4e-format "Decrypt message?")) + mu4e-decryption-policy)))) + (defun mu4e-headers-view-message () "View message at point . If there's an existing window for the view, re-use that one . If @@ -1546,6 +1769,8 @@ window . " (if (functionp mu4e-view-auto-mark-as-read) (funcall mu4e-view-auto-mark-as-read msg) mu4e-view-auto-mark-as-read)) + (decrypt (mu4e~decrypt-p msg)) + (verify mu4e-view-use-old) (viewwin (mu4e~headers-redraw-get-view-window))) (unless (window-live-p viewwin) (mu4e-error "Cannot get a message view")) @@ -1566,9 +1791,45 @@ window . " ;; (if mu4e-view-use-gnus ;; (mu4e-view msg) - ;; (mu4e--server-view dowcid decrypt)) - (mu4e--server-view docid mark-as-read))) + ;; (mu4e~proc-view dowcid decrypt)) + (mu4e~proc-view docid mark-as-read decrypt verify))) +(defun mu4e-headers-rerun-search () + "Rerun the search for the last search expression." + (interactive) + ;; if possible, try to return to the same message + (let* ((msg (mu4e-message-at-point t)) + (msgid (and msg (mu4e-message-field msg :message-id)))) + (mu4e-headers-search mu4e~headers-last-query nil nil t msgid))) + +(defun mu4e~headers-query-navigate (whence) + "Execute the previous query from the query stacks. +WHENCE determines where the query is taken from and is a symbol, +either `future' or `past'." + (let ((query (mu4e~headers-pop-query whence)) + (where (if (eq whence 'future) 'past 'future))) + (when query + (mu4e~headers-push-query mu4e~headers-last-query where) + (mu4e-headers-search query nil nil t)))) + +(defun mu4e-headers-query-next () + "Execute the previous query from the query stacks." + (interactive) + (mu4e~headers-query-navigate 'future)) + +(defun mu4e-headers-query-prev () + "Execute the previous query from the query stacks." + (interactive) + (mu4e~headers-query-navigate 'past)) + +;; forget the past so we don't repeat it :/ +(defun mu4e-headers-forget-queries () + "Forget all the complete query history." + (interactive) + (setq ;; note: don't forget the present one + mu4e~headers-query-past nil + mu4e~headers-query-future nil) + (mu4e-message "Query history cleared")) (defun mu4e~headers-move (lines) "Move point LINES lines forward (if LINES is positive) or @@ -1658,9 +1919,9 @@ given, offer to edit the search query before executing it." (list maildir current-prefix-arg))) (when maildir (let* ((query (format "maildir:\"%s\"" maildir)) - (query (if edit (mu4e-search-read-query "Refine query: " query) query))) + (query (if edit (mu4e-read-query "Refine query: " query) query))) (mu4e-mark-handle-when-leaving) - (mu4e-search query)))) + (mu4e-headers-search query)))) (defun mu4e-headers-split-view-grow (&optional n) "In split-view, grow the headers window. @@ -1740,32 +2001,6 @@ other windows." (kill-buffer) (mu4e~main-view 'refresh)))) - -;;; Loading messages -;; -(defvar mu4e-loading-mode-map nil "Keymap for *mu4e-loading* buffers.") -(unless mu4e-loading-mode-map - (setq mu4e-loading-mode-map - (let ((map (make-sparse-keymap))) - (define-key map "n" 'ignore) - (define-key map "p" 'ignore) - (define-key map "q" - (lambda()(interactive) - (if (eq mu4e-split-view 'single-window) - 'kill-buffer - 'kill-buffer-and-window))) - map))) -(fset 'mu4e-loading-mode-map mu4e-loading-mode-map) - -(define-derived-mode mu4e-loading-mode special-mode - "mu4e:loading" - (use-local-map mu4e-loading-mode-map) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert (propertize "Loading message..." - 'face 'mu4e-system-face 'intangible t)))) - - ;;; _ (provide 'mu4e-headers) ;;; mu4e-headers.el ends here diff --git a/mu4e/mu4e-helpers.el b/mu4e/mu4e-helpers.el deleted file mode 100644 index 7b533f89..00000000 --- a/mu4e/mu4e-helpers.el +++ /dev/null @@ -1,518 +0,0 @@ -;;; mu4e-helpers.el -- part of mu4e -*- lexical-binding: t -*- - -;; Copyright (C) 2021 Dirk-Jan C. Binnema - -;; Author: Dirk-Jan C. Binnema -;; Maintainer: 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 . - -;;; Commentary: - -;; Helper functions used in the mu4e. This is slowly usurp all the code from -;; mu4e-utils.el that does not depend on other parts of mu4e. - -;;; Code: - -(require 'seq) -(require 'ido) -(require 'cl-lib) - - -;;; Customization - -(defcustom mu4e-debug nil - "When set to non-nil, log debug information to the mu4e log buffer." - :type 'boolean - :group 'mu4e) - -(defcustom mu4e-modeline-max-width 42 - "Determines the maximum length of the modeline string. -If the string exceeds this limit, it will be truncated to fit." - :type 'integer - :group 'mu4e) - -(defcustom mu4e-completing-read-function 'ido-completing-read - "Function to be used to receive user-input during completion. -Suggested possible values are: - * `completing-read': built-in completion method - * `ido-completing-read': dynamic completion within the minibuffer." - :type 'function - :options '(completing-read ido-completing-read) - :group 'mu4e) - -(defcustom mu4e-use-fancy-chars nil - "When set, allow fancy (Unicode) characters for marks/threads. -You can customize the exact fancy characters used with -`mu4e-marks' and various `mu4e-headers-..-mark' and -`mu4e-headers..-prefix' variables." - :type 'boolean - :group 'mu4e) - -(defcustom mu4e-display-update-status-in-modeline nil - "Non-nil value will display the update status in the modeline." - :group 'mu4e - :type 'boolean) - -;; maybe move the next ones... but they're convenient -;; here because they're needed in multiple buffers. - -(defcustom mu4e-view-auto-mark-as-read t - "Automatically mark messages are 'read' when you read them. -This is the default behavior, but can be turned off, for example -when using a read-only file-system. - -This can also be set to a function; if so, receives a message -plist which should evaluate to nil if the message should *not* be -marked as read-only, or non-nil otherwise." - :type '(choice - boolean - function) - :group 'mu4e-view) - - -(defcustom mu4e-split-view 'horizontal - "How to show messages / headers. -A symbol which is either: - * `horizontal': split horizontally (headers on top) - * `vertical': split vertically (headers on the left). - * `single-window': view and headers in one window (mu4e will try not to - touch your window layout), main view in minibuffer - * anything else: don't split (show either headers or messages, - not both) -Also see `mu4e-headers-visible-lines' -and `mu4e-headers-visible-columns'." - :type '(choice (const :tag "Split horizontally" horizontal) - (const :tag "Split vertically" vertical) - (const :tag "Single window" single-window) - (const :tag "Don't split" nil)) - :group 'mu4e-headers) - -;;; Buffers - -(defconst mu4e-main-buffer-name " *mu4e-main*" - "Name of the mu4e main buffer. -The default name starts with SPC and therefore is not visible in -buffer list.") -(defconst mu4e-headers-buffer-name "*mu4e-headers*" - "Name of the buffer for message headers.") -(defconst mu4e-embedded-buffer-name " *mu4e-embedded*" - "Name for the embedded message view buffer.") - -(defun mu4e-get-headers-buffer() - "Get the name of the headers buffer." - (get-buffer mu4e-headers-buffer-name)) - -(defun mu4e-get-view-buffer() - "Get the name of the view buffer." - ;; avoid a 'require. - (when (boundp 'gnus-article-buffer) gnus-article-buffer)) - -(defun mu4e-select-other-view () - "Switch between headers view and message view." - (interactive) - (let* ((other-buf - (cond - ((eq major-mode 'mu4e-headers-mode) - (mu4e-get-view-buffer)) - ((eq major-mode 'mu4e-view-mode) - (mu4e-get-headers-buffer)))) - (other-win (and other-buf (get-buffer-window other-buf)))) - (if (window-live-p other-win) - (select-window other-win) - (mu4e-message "No window to switch to")))) - - -;;; Windows -(defun mu4e-hide-other-mu4e-buffers () - "Bury mu4e buffers. -Hide (main, headers, view) (and delete all windows displaying -it). Do _not_ bury the current buffer, though." - (interactive) - (unless (eq mu4e-split-view 'single-window) - (let ((curbuf (current-buffer))) - ;; note: 'walk-windows' does not seem to work correctly when modifying - ;; windows; therefore, the doloops here - (dolist (frame (frame-list)) - (dolist (win (window-list frame nil)) - (with-current-buffer (window-buffer win) - (unless (eq curbuf (current-buffer)) - (when (member major-mode '(mu4e-headers-mode mu4e-view-mode)) - (when (eq t (window-deletable-p win)) - (delete-window win))))))) t))) - -;;; Modeline - -(defun mu4e-quote-for-modeline (str) - "Quote STR to be used literally in the modeline. -The string will be shortened to fit if its length exceeds -`mu4e-modeline-max-width'." - (replace-regexp-in-string - "%" "%%" - (truncate-string-to-width str mu4e-modeline-max-width 0 nil t))) - - - -;;; Messages, warnings and errors -(defun mu4e-format (frm &rest args) - "Create [mu4e]-prefixed string based on format FRM and ARGS." - (concat - "[" (propertize "mu4e" 'face 'mu4e-title-face) "] " - (apply 'format frm - (mapcar (lambda (x) - (if (stringp x) - (decode-coding-string x 'utf-8) - x)) - args)))) - -(defun mu4e-message (frm &rest args) - "Display FRM with ARGS like `message' in mu4e style. -If we're waiting for user-input or if there's some message in the -echo area, don't show anything." - (unless (or (active-minibuffer-window)) - (message "%s" (apply 'mu4e-format frm args)))) - -(defun mu4e-error (frm &rest args) - "Display an error with FRM and ARGS like `mu4e-message'. - -Create [mu4e]-prefixed error based on format FRM and ARGS. Does a -local-exit and does not return, and raises a -debuggable (backtrace) error." - (mu4e-log 'error (apply 'mu4e-format frm args)) - (error "%s" (apply 'mu4e-format frm args))) - -(defun mu4e-warn (frm &rest args) - "Create [mu4e]-prefixed warning based on format FRM and ARGS. -Does a local-exit and does not return." - (mu4e-log 'error (apply 'mu4e-format frm args)) - (user-error "%s" (apply 'mu4e-format frm args))) - -;;; Reading user input - -(defun mu4e--read-char-choice (prompt choices) - "Read and return one of CHOICES, prompting for PROMPT. -Any input that is not one of CHOICES is ignored. This mu4e's -version of `read-char-choice' which becomes case-insentive after -trying an exact match." - (let ((choice) (chosen) (inhibit-quit nil)) - (while (not chosen) - (message nil);; this seems needed... - (setq choice (read-char-exclusive prompt)) - (if (eq choice 27) (keyboard-quit)) ;; quit if ESC is pressed - (setq chosen (or (member choice choices) - (member (downcase choice) choices) - (member (upcase choice) choices)))) - (car chosen))) - -(defun mu4e-read-option (prompt options) - "Ask user for an option from a list on the input area. -PROMPT describes a multiple-choice question to the user. -OPTIONS describe the options, and is a list of cells describing -particular options. Cells have the following structure: - - (OPTIONSTRING . RESULT) - -where OPTIONSTRING is a non-empty string describing the -option. The first character of OPTIONSTRING is used as the -shortcut, and obviously all shortcuts must be different, so you -can prefix the string with an uniquifying character. - -The options are provided as a list for the user to choose from; -user can then choose by typing CHAR. Example: - (mu4e-read-option \"Choose an animal: \" - '((\"Monkey\" . monkey) (\"Gnu\" . gnu) (\"xMoose\" . moose))) - -User now will be presented with a list: \"Choose an animal: - [M]onkey, [G]nu, [x]Moose\". - -Function will return the cdr of the list element." - (let* ((prompt (mu4e-format "%s" prompt)) - (optionsstr - (mapconcat - (lambda (option) - ;; try to detect old-style options, and warn - (when (characterp (car-safe (cdr-safe option))) - (mu4e-error - (concat "Please use the new format for options/actions; " - "see the manual"))) - (let ((kar (substring (car option) 0 1))) - (concat - "[" (propertize kar 'face 'mu4e-highlight-face) "]" - (substring (car option) 1)))) - options ", ")) - (response - (mu4e--read-char-choice - (concat prompt optionsstr - " [" (propertize "C-g" 'face 'mu4e-highlight-face) - " to cancel]") - ;; the allowable chars - (seq-map (lambda(elm) (string-to-char (car elm))) options))) - (chosen - (seq-find - (lambda (option) (eq response (string-to-char (car option)))) - options))) - (if chosen - (cdr chosen) - (mu4e-warn "Unknown shortcut '%c'" response)))) - - - -;;; Server properties -(defvar mu4e--server-props nil - "Metadata we receive from the mu4e server.") - -(defun mu4e-server-properties () - "Get the server metadata plist." - mu4e--server-props) - -(defun mu4e-root-maildir() - "Get the root maildir." - (or (and mu4e--server-props - (plist-get mu4e--server-props :root-maildir)) - (mu4e-error "Root maildir unknown; did you start mu4e?"))) - -(defun mu4e-database-path() - "Get the root maildir." - (or (and mu4e--server-props - (plist-get mu4e--server-props :database-path)) - (mu4e-error "Root maildir unknown; did you start mu4e?"))) - -(defun mu4e-server-version() - "Get the root maildir." - (or (and mu4e--server-props - (plist-get mu4e--server-props :version)) - (mu4e-error "Version unknown; did you start mu4e?"))) - -(defun mu4e-last-query-results () - "Get the results (counts) of the last cached queries. - -The cached queries are the bookmark / maildir queries that are -used to populated the read/unread counts in the main view. They -are refreshed when calling `(mu4e)', i.e., when going to the main -view. - -The results are a list of elements of the form - (:query \"query string\" - :count - :unread )" - (plist-get mu4e--server-props :queries)) - -(defun mu4e-last-query-result (query) - "Get the last result for some QUERY or nil if not found." - (seq-find - (lambda (elm) (string= (plist-get elm :query) query)) - (mu4e-last-query-results))) - - -;;; Logging / debugging - -(defconst mu4e--log-max-size 1000000 - "Max number of characters to keep around in the log buffer.") -(defconst mu4e--log-buffer-name "*mu4e-log*" - "Name of the logging buffer.") - -(defun mu4e--get-log-buffer () - "Fetch (and maybe create) the log buffer." - (unless (get-buffer mu4e--log-buffer-name) - (with-current-buffer (get-buffer-create mu4e--log-buffer-name) - (view-mode) - (when (fboundp 'so-long-mode) - (unless (eq major-mode 'so-long-mode) - (eval '(so-long-mode)))) - (setq buffer-undo-list t))) - mu4e--log-buffer-name) - -(defun mu4e-log (type frm &rest args) - "Log a message of TYPE with format-string FRM and ARGS. -Use the mu4e log buffer for this. If the variable mu4e-debug is -non-nil. Type is either 'to-server, 'from-server or 'misc. This -function is meant for debugging." - (when mu4e-debug - (with-current-buffer (mu4e--get-log-buffer) - (let* ((inhibit-read-only t) - (tstamp (propertize (format-time-string "%Y-%m-%d %T.%3N" - (current-time)) - 'face 'font-lock-string-face)) - (msg-face - (cl-case type - (from-server 'font-lock-type-face) - (to-server 'font-lock-function-name-face) - (misc 'font-lock-variable-name-face) - (error 'font-lock-warning-face) - (otherwise (mu4e-error "Unsupported log type")))) - (msg (propertize (apply 'format frm args) 'face msg-face))) - (save-excursion - (goto-char (point-max)) - (insert tstamp - (cl-case type - (from-server " <- ") - (to-server " -> ") - (error " !! ") - (otherwise " ")) - msg "\n") - ;; if `mu4e-log-max-lines is specified and exceeded, clearest the - ;; oldest lines - (when (> (buffer-size) mu4e--log-max-size) - (goto-char (- (buffer-size) mu4e--log-max-size)) - (beginning-of-line) - (delete-region (point-min) (point)))))))) - -(defun mu4e-toggle-logging () - "Toggle `mu4e-debug'. -In debug-mode, mu4e logs some of its internal workings to a -log-buffer. See `mu4e-show-log'." - (interactive) - (mu4e-log 'misc "logging disabled") - (setq mu4e-debug (not mu4e-debug)) - (mu4e-message "debug logging has been %s" - (if mu4e-debug "enabled" "disabled")) - (mu4e-log 'misc "logging enabled")) - -(defun mu4e-show-log () - "Visit the mu4e debug log." - (interactive) - (unless mu4e-debug (mu4e-toggle-logging)) - (let ((buf (get-buffer mu4e--log-buffer-name))) - (unless (buffer-live-p buf) - (mu4e-warn "No debug log available")) - (switch-to-buffer buf))) - - - -;;; Flags -;; Converting flags->string and vice-versa - -(defun mu4e-flags-to-string (flags) - "Convert a list of Maildir[1] FLAGS into a string. - -See `mu4e-string-to-flags'. \[1\]: -http://cr.yp.to/proto/maildir.html." - (seq-sort - '< - (seq-mapcat - (lambda (flag) - (pcase flag - (`draft "D") - (`flagged "F") - (`new "N") - (`passed "P") - (`replied "R") - (`seen "S") - (`trashed "T") - (`attach "a") - (`encrypted "x") - (`signed "s") - (`unread "u") - (_ ""))) - (seq-uniq flags) 'string))) - -(defun mu4e-string-to-flags (str) - "Convert a STR with Maildir[1] flags into a list of flags. - -See `mu4e-string-to-flags'. \[1\]: -http://cr.yp.to/proto/maildir.html." - (seq-uniq - (seq-filter - 'identity - (seq-mapcat - (lambda (kar) - (list - (pcase kar - ('?D 'draft) - ('?F 'flagged) - ('?P 'passed) - ('?R 'replied) - ('?S 'seen) - ('?T 'trashed) - (_ nil)))) - str)))) - - -;;; Misc -(defun mu4e-display-size (size) - "Get a human-friendly string representation of SIZE (in bytes)." - (cond - ((>= size 1000000) - (format "%2.1fM" (/ size 1000000.0))) - ((and (>= size 1000) (< size 1000000)) - (format "%2.1fK" (/ size 1000.0))) - ((< size 1000) - (format "%d" size)) - (t "?"))) - - -(defun mu4e-split-ranges-to-numbers (str n) - "Convert STR containing attachment numbers into a list of numbers. - -STR is a string; N is the highest possible number in the list. -This includes expanding e.g. 3-5 into 3,4,5. If the letter -\"a\" ('all')) is given, that is expanded to a list with numbers -[1..n]." - (let ((str-split (split-string str)) - beg end list) - (dolist (elem str-split list) - ;; special number "a" converts into all attachments 1-N. - (when (equal elem "a") - (setq elem (concat "1-" (int-to-string n)))) - (if (string-match "\\([0-9]+\\)-\\([0-9]+\\)" elem) - ;; we have found a range A-B, which needs converting - ;; into the numbers A, A+1, A+2, ... B. - (progn - (setq beg (string-to-number (match-string 1 elem)) - end (string-to-number (match-string 2 elem))) - (while (<= beg end) - (cl-pushnew beg list :test 'equal) - (setq beg (1+ beg)))) - ;; else just a number - (cl-pushnew (string-to-number elem) list :test 'equal))) - ;; Check that all numbers are valid. - (mapc - (lambda (x) - (cond - ((> x n) - (mu4e-warn "Attachment %d bigger than maximum (%d)" x n)) - ((< x 1) - (mu4e-warn "Attachment number must be greater than 0 (%d)" x)))) - list))) - -(defun mu4e-make-temp-file (ext) - "Create a self-destructing temporary file with extension EXT. -The file will self-destruct in a short while, enough to open it -in an external program." - (let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext)))) - (run-at-time "30 sec" nil - (lambda () (ignore-errors (delete-file tmpfile)))) - tmpfile)) - -(defun mu4e-display-manual () - "Display the mu4e manual page for the current mode. -Or go to the top level if there is none." - (interactive) - (info (cl-case major-mode - ('mu4e-main-mode "(mu4e)Main view") - ('mu4e-headers-mode "(mu4e)Headers view") - ('mu4e-view-mode "(mu4e)Message view") - (t "mu4e")))) - - ;;; Macros - -(defmacro mu4e-setq-if-nil (var val) - "Set VAR to VAL if VAR is nil." - `(unless ,var (setq ,var ,val))) - -(provide 'mu4e-helpers) -;;; mu4e-helpers.el ends here - diff --git a/mu4e/mu4e-icalendar.el b/mu4e/mu4e-icalendar.el index 3b5bef8f..7feceaf2 100644 --- a/mu4e/mu4e-icalendar.el +++ b/mu4e/mu4e-icalendar.el @@ -53,31 +53,14 @@ (require 'cl-lib) (require 'mu4e-mark) -(require 'mu4e-helpers) -(require 'mu4e-contacts) +(require 'mu4e-utils) (require 'mu4e-headers) (require 'mu4e-view) +(require 'mu4e-vars) - -;;; Configuration -;;;; Calendar +(when mu4e-view-use-old + (mu4e-error "iCalender support is not available with the old viewer")) -(defgroup mu4e-icalendar nil - "Icalendar related settings." - :group 'mu4e) - -(defcustom mu4e-icalendar-trash-after-reply nil - "If non-nil, trash the icalendar invitation after replying." - :type 'boolean - :group 'mu4e-icalendar) - -(defcustom mu4e-icalendar-diary-file nil - "If non-nil, the file in which to add events upon reply." - :type '(choice (const :tag "Do not insert a diary entry" nil) - (string :tag "Insert a diary entry in this file")) - :group 'mu4e-icalendar) - - ;;;###autoload (defun mu4e-icalendar-setup () "Perform the necessary initialization to use mu4e-icalendar." diff --git a/mu4e/mu4e-lists.el b/mu4e/mu4e-lists.el index fa7333f4..654ada33 100644 --- a/mu4e/mu4e-lists.el +++ b/mu4e/mu4e-lists.el @@ -1,6 +1,6 @@ -;;; mu4e-lists.el -- part of mu4e -*- lexical-binding: t -*- +;;; mu4e-lists.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- -;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema +;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema @@ -27,10 +27,7 @@ ;;; Code: -(require 'cl-lib) - -;;; Configuration -(defvar mu4e-mailing-lists +(defvar mu4e~mailing-lists '( ("bbdb-info.lists.sourceforge.net" . "BBDB") ("boost-announce.lists.boost.org" . "BoostA") ("boost-interest.lists.boost.org" . "BoostI") @@ -84,50 +81,22 @@ ("wl-en.ml.gentei.org" . "WdrLust") ("xapian-devel.lists.xapian.org" . "Xapian") ("zsh-users.zsh.org" . "ZshUsr")) - "AList of cells (MAILING-LIST-ID . SHORTNAME).") + "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'." + "An alist with cells (MAILING-LIST-ID . SHORTNAME); these are +used in addition to the built-in list `mu4e~mailing-lists'." :group 'mu4e-headers :type '(repeat (cons string string))) + (defcustom mu4e-mailing-list-patterns nil - "A list of regexps to capture a shortname out of a list-id. -For the first regex that matches, its first matchgroup will be -used as the shortname." + "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))) - -(defvar mu4e--lists-hash nil - "Hashtable of mailing-list-id => shortname. -Based on `mu4e-mailing-lists' and `mu4e-user-mailing-lists'.") - - -(defun mu4e-get-mailing-list-shortname (list-id) - "Get the shortname for a mailing-list with list-id LIST-ID. -Based on `mu4e-mailing-lists', `mu4e-user-mailing-lists', and -`mu4e-mailing-list-patterns'." - (unless mu4e--lists-hash - (setq mu4e--lists-hash (make-hash-table :test 'equal)) - (dolist (cell mu4e-mailing-lists) - (puthash (car cell) (cdr cell) mu4e--lists-hash)) - (dolist (cell mu4e-user-mailing-lists) - (puthash (car cell) (cdr cell) mu4e--lists-hash))) - (or - (gethash list-id mu4e--lists-hash) - (and (boundp 'mu4e-mailing-list-patterns) - (cl-member-if - (lambda (pattern) - (string-match pattern list-id)) - mu4e-mailing-list-patterns) - (match-string 1 list-id)) - ;; if it's not in the db, take the part until the first dot if there is one; - ;; otherwise just return the whole thing - (if (string-match "\\([^.]*\\)\\." list-id) - (match-string 1 list-id) - list-id))) ;;; _ (provide 'mu4e-lists) ;;; mu4e-lists.el ends here diff --git a/mu4e/mu4e-main.el b/mu4e/mu4e-main.el index 58d417d3..839ccf6b 100644 --- a/mu4e/mu4e-main.el +++ b/mu4e/mu4e-main.el @@ -1,6 +1,6 @@ ;;; mu4e-main.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- -;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema +;; Copyright (C) 2011-2020 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema @@ -25,20 +25,12 @@ ;;; Code: (require 'smtpmail) ;; the queueing stuff (silence elint) -(require 'mu4e-helpers) ;; utility functions +(require 'mu4e-utils) ;; utility functions (require 'mu4e-context) ;; the context -(require 'mu4e-bookmarks) -(require 'mu4e-folders) -(require 'mu4e-update) -(require 'mu4e-contacts) -(require 'mu4e-search) (require 'mu4e-vars) ;; mu-wide variables - (require 'cl-lib) - - -;; Configuration +;;; Mode (define-obsolete-variable-alias 'mu4e-main-buffer-hide-personal-addresses @@ -51,44 +43,19 @@ 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.") - -;;; Mode -(define-derived-mode mu4e-org-mode org-mode "mu4e:org" - "Major mode for mu4e documents, derived from - `org-mode'.") - -(defun mu4e-info (path) - "Show a buffer with the information (an org-file) at PATH." - (unless (file-exists-p path) - (mu4e-error "Cannot find %s" path)) - (let ((curbuf (current-buffer))) - (find-file path) - (mu4e-org-mode) - (setq buffer-read-only t) - (define-key mu4e-org-mode-map (kbd "q") - `(lambda () - (interactive) - (bury-buffer) - (switch-to-buffer ,curbuf))))) - -(defun mu4e-about () - "Show the mu4e 'about' page." - (interactive) - (mu4e-info (concat mu4e-doc-dir "/mu4e-about.org"))) - -(defun mu4e-news () - "Show the mu4e 'about' page." - (interactive) - (mu4e-info (concat mu4e-doc-dir "/NEWS.org"))) - - (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) @@ -120,9 +87,7 @@ no unread messages.") \\{mu4e-main-mode-map}." (setq truncate-lines t overwrite-mode 'overwrite-mode-binary) - (mu4e-context-minor-mode) - (mu4e-search-minor-mode) - (mu4e-update-minor-mode) + (mu4e-context-in-modeline) (set (make-local-variable 'revert-buffer-function) #'mu4e~main-view-real)) @@ -159,23 +124,15 @@ clicked." 'mouse-face 'highlight newstr) newstr)) - - -(defun mu4e--longest-of-maildirs-and-bookmarks () - "Return the length of longest name of bookmarks and maildirs." - (cl-loop for b in (append (mu4e-bookmarks) - (mu4e--maildirs-with-query)) - maximize (string-width (plist-get b :name)))) - (defun mu4e~main-bookmarks () ;; 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 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-search-query-rewrite-function #'identity) + for query = (funcall (or mu4e-query-rewrite-function #'identity) (plist-get bm :query)) for qcounts = (and (stringp query) (cl-loop for q in queries @@ -208,9 +165,9 @@ clicked." (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) + (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) @@ -263,15 +220,13 @@ clicked." "The revert buffer function for `mu4e-main-mode'." (mu4e~main-view-real-1 'refresh)) -(declare-function mu4e--start "mu4e") - (defun mu4e~main-view-real-1 (&optional refresh) "Create `mu4e-main-buffer-name' and set it up. When REFRESH is non nil refresh infos from server." (let ((inhibit-read-only t)) ;; Maybe refresh infos from server. (if refresh - (mu4e--start 'mu4e~main-redraw-buffer) + (mu4e~start 'mu4e~main-redraw-buffer) (mu4e~main-redraw-buffer)))) (defun mu4e~main-redraw-buffer () @@ -323,7 +278,7 @@ When REFRESH is non nil refresh infos from server." (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") + (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")))) diff --git a/mu4e/mu4e-mark.el b/mu4e/mu4e-mark.el index 8a44bfc4..7e2b6c1d 100644 --- a/mu4e/mu4e-mark.el +++ b/mu4e/mu4e-mark.el @@ -28,10 +28,9 @@ ;;; Code: (require 'cl-lib) -(require 'mu4e-server) +(require 'mu4e-proc) (require 'mu4e-utils) (require 'mu4e-message) -(require 'mu4e-folders) ;; keep byte-compiler happy (declare-function mu4e~headers-mark "mu4e-headers") @@ -133,50 +132,50 @@ The current buffer must be either a headers or view buffer." :prompt "refile" :dyn-target (lambda (target msg) (mu4e-get-refile-folder msg)) :action (lambda (docid msg target) - (mu4e--server-move docid (mu4e~mark-check-target target) "-N"))) + (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--server-remove docid))) + :action (lambda (docid msg target) (mu4e~proc-remove docid))) (flag :char ("+" . "✚") :prompt "+flag" :show-target (lambda (target) "flag") :action (lambda (docid msg target) - (mu4e--server-move docid nil "+F-u-N"))) + (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--server-move docid (mu4e~mark-check-target target) "-N"))) + (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--server-move docid nil "+S-u-N"))) + :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--server-move docid + :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--server-move docid nil "-F-N"))) + :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--server-move docid nil "-T"))) + :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--server-move docid nil "-S+u-N"))) + :action (lambda (docid msg target) (mu4e~proc-move docid nil "-S+u-N"))) (unmark :char " " :prompt "unmark" @@ -299,7 +298,7 @@ The following marks are available, and the corresponding props: (when (or (file-directory-p fulltarget) (and (yes-or-no-p (format "%s does not exist. Create now?" fulltarget)) - (mu4e--server-mkdir fulltarget))) + (mu4e~proc-mkdir fulltarget))) target))) (defun mu4e~mark-ask-target (mark) diff --git a/mu4e/mu4e-message.el b/mu4e/mu4e-message.el index 492719cc..84014801 100644 --- a/mu4e/mu4e-message.el +++ b/mu4e/mu4e-message.el @@ -38,14 +38,52 @@ (defvar mu4e~view-message) (defvar shr-inhibit-images) - -(make-obsolete-variable 'mu4e-html2text-command "No longer in use" "1.7.0") -(make-obsolete-variable 'mu4e-view-prefer-html "No longer in use" "1.7.0") -(make-obsolete-variable 'mu4e-view-html-plaintext-ratio-heuristic - "No longer in use" "1.7.0") -(make-obsolete-variable 'mu4e-message-body-rewrite-functions - "No longer in use" "1.7.0") - + +(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) @@ -131,11 +169,89 @@ This is equivalent to: (mu4e-message-field (mu4e-message-at-point) FIELD)." (mu4e-message-field (mu4e-message-at-point) field)) -(defun mu4e-message-body-text (_msg &optional _prefer-html) - "Get the body in text form for message MSG." - "" ;; not implemented for Gnus mode. -) - +(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 "\015 ’]" 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? @@ -168,8 +284,7 @@ expressions, in which case any of those are tried for a match." (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 + "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 @@ -178,20 +293,19 @@ cell that matched, or nil." (mu4e-message-field msg cfield))) (defun mu4e-message-sent-by-me (msg) - "Is this MSG (to be) sent by me? + "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 MSG have user's personal address? -In any of the contact - fields?" + "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) +(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)." @@ -208,14 +322,41 @@ symbol, see `mu4e-header-info'." (plist-get (mu4e-message-at-point) field)) ;;; Html2Text -(make-obsolete 'mu4e-shr2text "No longer in use" "1.7.0") -(defun mu4e-copy-message-path () - "Copy the message-path of message at point to the kill ring." - (interactive) - (let ((path (mu4e-message-field-at-point :path))) - (kill-new path) - (mu4e-message "Saved '%s' to kill-ring" path))) +(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) diff --git a/mu4e/mu4e-org.el b/mu4e/mu4e-org.el index 7fbf7459..7f20573b 100644 --- a/mu4e/mu4e-org.el +++ b/mu4e/mu4e-org.el @@ -122,7 +122,7 @@ the query (for links starting with 'query:')." ((string-match "^msgid:\\(.+\\)" link) (mu4e-view-message-with-message-id (match-string 1 link))) ((string-match "^query:\\(.+\\)" link) - (mu4e-search (match-string 1 link) current-prefix-arg)) + (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") diff --git a/mu4e/mu4e-server.el b/mu4e/mu4e-proc.el similarity index 56% rename from mu4e/mu4e-server.el rename to mu4e/mu4e-proc.el index c0aff6eb..b7224104 100644 --- a/mu4e/mu4e-server.el +++ b/mu4e/mu4e-proc.el @@ -1,6 +1,6 @@ -;;; mu4e-server.el -- part of mu4e -*- lexical-binding: t -*- +;;; mu4e-proc.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- -;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema +;; Copyright (C) 2011-2020 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema @@ -24,175 +24,72 @@ ;;; Code: -(require 'mu4e-helpers) +(require 'mu4e-vars) +(require 'mu4e-utils) (require 'mu4e-meta) - -;;; Configuration -(defcustom mu4e-mu-home nil - "Location of an alternate mu home dir. -If not set, use the defaults, based on the XDG Base Directory -Specification." - :group 'mu4e - :type '(choice (const :tag "Default location" nil) - (directory :tag "Specify location")) - :safe 'stringp) - -(defcustom mu4e-mu-binary (executable-find "mu") - "Name of the mu-binary to use. -If it cannot be found in your PATH, you can specify the full -path." - :type 'file - :group 'mu4e - :safe 'stringp) - -(defcustom mu4e-mu-debug nil - "Whether to run the mu binary in debug-mode. -Setting this to t increases the amount of information in the log." - :type 'boolean - :group 'mu4e) - -(make-obsolete-variable - 'mu4e-maildir - "determined by server; see `mu4e-root-maildir'." "1.3.8") - -(defcustom mu4e-change-filenames-when-moving nil - "Change message file names when moving them. -When moving messages to different folders, normally mu/mu4e keep -the base filename the same (the flags-part of the filename may -change still). With this option set to non-nil, mu4e instead -changes the filename. This latter behavior works better with some -IMAP-synchronization programs such as mbsync; the default works -better with e.g. offlineimap." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - - -;; Handlers are not strictly internal, but are not meant -;; for overriding outside mu4e. The are mainly for breaking -;; dependency cycles. - -(defvar mu4e-error-func nil - "Function called for each error received. -The function is passed an error plist as argument. See -`mu4e--server-filter' for the format.") - -(defvar mu4e-update-func nil - "Function called for each :update sexp returned. -The function is passed a msg sexp as argument. -See `mu4e--server-filter' for the format.") - -(defvar mu4e-remove-func nil - "Function called for each :remove sexp returned. -This happens when some message has been deleted. The function is -passed the docid of the removed message.") - -(defvar mu4e-sent-func nil - "Function called for each :sent sexp received. -This happens when some message has been sent. The function is -passed the docid and the draft-path of the sent message.") - -(defvar mu4e-view-func nil - "Function called for each single-message sexp. -The function is passed a message sexp as argument. See -`mu4e--server-filter' for the format.") - -(defvar mu4e-header-func nil - "Function called for each message-header received. -The function is passed a msg plist as argument. See -`mu4e--server-filter' for the format.") - -(defvar mu4e-found-func nil - "Function called for when we received a :found sexp. -This happens after the headers have been returned, to report on -the number of matches. See `mu4e--server-filter' for the format.") - -(defvar mu4e-erase-func nil - "Function called we receive an :erase sexp. -This before new headers are displayed, to clear the current -headers buffer. See `mu4e--server-filter' for the format.") - -(defvar mu4e-compose-func nil - "Function called for each compose message received. -I.e., the original message that is used as basis for composing a -new message (i.e., either a reply or a forward); the function is -passed msg and a symbol (either reply or forward). See -`mu4e--server-filter' for the format of .") - -(defvar mu4e-info-func nil - "Function called for each (:info type ....) sexp received. -from the server process.") - -(defvar mu4e-pong-func nil - "Function called for each (:pong type ....) sexp received.") - -(defvar mu4e-contacts-func nil - "A function called for each (:contacts () -sexp received from the server process.") - -(make-obsolete-variable 'mu4e-temp-func "No longer used" "1.7.0") - ;;; Internal vars -(defvar mu4e--server-buf nil +(defvar mu4e~proc-buf nil "Buffer (string) for data received from the backend.") -(defconst mu4e--server-name " *mu4e-server*" +(defconst mu4e~proc-name " *mu4e-proc*" "Name of the server process, buffer.") -(defvar mu4e--server-process nil +(defvar mu4e~proc-process nil "The mu-server process.") ;; dealing with the length cookie that precedes expressions -(defconst mu4e--server-cookie-pre "\376" +(defconst mu4e~cookie-pre "\376" "Each expression starts with a length cookie: -<`mu4e--server-cookie-pre'><`mu4e--server-cookie-post'>.") -(defconst mu4e--server-cookie-post "\377" +<`mu4e~cookie-pre'><`mu4e~cookie-post'>.") +(defconst mu4e~cookie-post "\377" "Each expression starts with a length cookie: -<`mu4e--server-cookie-pre'><`mu4e--server-cookie-post'>.") -(defconst mu4e--server-cookie-matcher-rx - (concat mu4e--server-cookie-pre "\\([[:xdigit:]]+\\)" - mu4e--server-cookie-post) +<`mu4e~cookie-pre'><`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).") - -(defun mu4e-running-p () - "Whether mu4e is running. -Checks whether the server process is live." - (and mu4e--server-process - (memq (process-status mu4e--server-process) + +;;; 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--server-eat-sexp-from-buf () - "'Eat' the next s-expression from `mu4e--server-buf'. -Note: this is a string, not an emacs-buffer. `mu4e--server-buf gets +(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--server-cookie-pre'><`mu4e--server-cookie-post'> + <`mu4e~cookie-pre'><`mu4e~cookie-post'> Function returns this sexp, or nil if there was none. -`mu4e--server-buf' is updated as well, with all processed sexp data +`mu4e~proc-buf' is updated as well, with all processed sexp data removed." (ignore-errors ;; the server may die in the middle... - (let ((b (string-match mu4e--server-cookie-matcher-rx mu4e--server-buf)) + ;; 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--server-buf) 16)) - ;; does mu4e--server-buf contain the full sexp? - (when (>= (length mu4e--server-buf) (+ sexp-len (match-end 0))) + (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--server-buf (substring mu4e--server-buf (match-end 0))) + (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--server-buf 0 sexp-len) + (substring mu4e~proc-buf 0 sexp-len) 'utf-8 t))) (when objcons - (setq mu4e--server-buf (substring mu4e--server-buf sexp-len)) + (setq mu4e~proc-buf (substring mu4e~proc-buf sexp-len)) (car objcons))))))) -(defun mu4e--server-filter (_proc str) +(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' @@ -250,8 +147,8 @@ The server output is as follows: (:compose [:original] [:include ]) `mu4e-compose-func'." (mu4e-log 'misc "* Received %d byte(s)" (length str)) - (setq mu4e--server-buf (concat mu4e--server-buf str)) ;; update our buffer - (let ((sexp (mu4e--server-eat-sexp-from-buf))) + (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) @@ -280,7 +177,6 @@ The server output is as follows: ;; received a pong message ((plist-get sexp :pong) - (setq mu4e--server-props (plist-get sexp :props)) (funcall mu4e-pong-func sexp)) ;; received a contacts message @@ -308,6 +204,15 @@ The server output is as follows: (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)) @@ -320,65 +225,71 @@ The server output is as follows: (t (mu4e-message "Unexpected data from server [%S]" sexp))) - (setq sexp (mu4e--server-eat-sexp-from-buf)))))) + (setq sexp (mu4e~proc-eat-sexp-from-buf)))))) -(defun mu4e--server-start () +(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")))) + (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' " + "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--server-buf "") - (setq mu4e--server-process (apply 'start-process - mu4e--server-name mu4e--server-name - mu4e-mu-binary 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--server-process + (unless mu4e~proc-process (mu4e-error "Failed to start the mu4e backend")) - (set-process-query-on-exit-flag mu4e--server-process nil) - (set-process-coding-system mu4e--server-process 'binary 'utf-8-unix) - (set-process-filter mu4e--server-process 'mu4e--server-filter) - (set-process-sentinel mu4e--server-process 'mu4e--server-sentinel))) + (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--server-kill () +(defun mu4e~proc-kill () "Kill the mu server process." - (let* ((buf (get-buffer mu4e--server-name)) + (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--server-call-mu '(quit))) + (mu4e~call-mu '(quit))) ;; try sending SIGINT (C-c) to process, so it can exit gracefully (ignore-errors (signal-process proc 'SIGINT)))) (setq - mu4e--server-process nil - mu4e--server-buf nil)) + 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--server-sentinel (proc _msg) +(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--server-process nil) - (setq mu4e--server-buf "") ;; clear any half-received sexps + (setq mu4e~proc-process nil) + (setq mu4e~proc-buf "") ;; clear any half-received sexps (cond ((eq status 'signal) (cond @@ -395,21 +306,27 @@ The server output is as follows: (t (error "Something bad happened to the mu server process"))))) -(defun mu4e--server-call-mu (form) - "Call the mu server with some command FORM." - (unless (mu4e-running-p) (mu4e--server-start)) +(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--server-process (concat cmd "\n")))) + (process-send-string mu4e~proc-process (concat cmd "\n")))) -(defun mu4e--server-add (path) +(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 :docid )' as well as `'(:update )`'; otherwise, we receive an error." - (mu4e--server-call-mu `(add :path ,path))) + (mu4e~call-mu `(add :path ,path))) -(defun mu4e--server-compose (type decrypt &optional docid) +(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, @@ -417,26 +334,42 @@ editing, resending) with DOCID or nil for type `new'. The result is delivered to the function registered as `mu4e-compose-func'." - (mu4e--server-call-mu - `(compose - :type ,type - :decrypt ,(and decrypt t) - :docid ,docid))) + (mu4e~call-mu `(compose + :type ,type + :decrypt ,(and decrypt t) + :docid ,docid))) -(defun mu4e--server-contacts (personal after tstamp) +(defun mu4e~proc-contacts (personal after tstamp) "Ask for contacts with PERSONAL AFTER TSTAMP. S-expression (:contacts () :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--server-call-mu - `(contacts - :personal ,(and personal t) - :after ,(or after nil) - :tstamp ,(or tstamp nil)))) + (mu4e~call-mu `(contacts + :personal ,(and personal t) + :after ,(or after nil) + :tstamp ,(or tstamp nil)))) -(defun mu4e--server-find (query threads sortfield sortdir maxnum skip-dups - include-related) +(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 :what :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 @@ -452,25 +385,26 @@ 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--server-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))) + (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--server-index (&optional cleanup lazy-check) +(defun mu4e~proc-index (&optional cleanup lazy-check) "Index messages with possible CLEANUP and LAZY-CHECK." - (mu4e--server-call-mu `(index :cleanup ,cleanup :lazy-check ,lazy-check))) + (mu4e~call-mu `(index :cleanup ,cleanup :lazy-check ,lazy-check))) -(defun mu4e--server-mkdir (path) +(defun mu4e~proc-mkdir (path) "Create a new maildir-directory at filesystem PATH." - (mu4e--server-call-mu `(mkdir :path ,path))) + ;;(mu4e~proc-send-command "cmd:mkdir path:%s" (mu4e~escape path)) + (mu4e~call-mu `(mkdir :path ,path))) -(defun mu4e--server-move (docid-or-msgid &optional maildir flags no-view) + +(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. @@ -508,46 +442,60 @@ Returns either (:update ... ) or (:error ) sexp, which are handled my (unless (or (not maildir) (file-exists-p (concat (mu4e-root-maildir) "/" maildir "/"))) (mu4e-error "Target dir does not exist")) - (mu4e--server-call-mu - `(move - :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid) - :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil) - :flags ,(or flags nil) - :maildir ,(or maildir nil) - :rename ,(and maildir mu4e-change-filenames-when-moving t) - :no-view ,(and no-view t)))) + (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--server-ping (&optional queries) +(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--server-call-mu `(ping :queries ,queries))) + (mu4e~call-mu `(ping :queries ,queries))) -(defun mu4e--server-remove (docid) +(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--server-call-mu `(remove :docid ,docid))) + (mu4e~call-mu `(remove :docid ,docid))) -(defun mu4e--server-sent (path) - "Tell the mu server we sent a message at PATH. -If this works, we will receive (:info add :path :docid +(defun mu4e~proc-sent (path) + "Add the message at PATH to the database. + + if this works, we will receive (:info add :path :docid :fcc )." - (mu4e--server-call-mu `(sent :path ,path))) + (mu4e~call-mu `(sent :path ,path))) -(defun mu4e--server-view (docid-or-msgid &optional mark-as-read) +(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. -The result will be delivered to the function registered as -`mu4e-view-func'." - (mu4e--server-call-mu - `(view - :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid) - :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil) - :mark-as-read ,mark-as-read))) +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)))) - -(provide 'mu4e-server) -;;; mu4e-server.el ends here +(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 diff --git a/mu4e/mu4e-search.el b/mu4e/mu4e-search.el deleted file mode 100644 index 55393123..00000000 --- a/mu4e/mu4e-search.el +++ /dev/null @@ -1,446 +0,0 @@ -;;; mu4e-search.el -- part of mu4e -*- lexical-binding: t -*- - -;; Copyright (C) 2021 Dirk-Jan C. Binnema - -;; Author: Dirk-Jan C. Binnema -;; Maintainer: 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 . - -;;; Commentary: - -;; Search-related functions and a minor-mode. - -;;; Code: - -(require 'seq) -(require 'cl-lib) -(require 'mu4e-helpers) - - -;;; Configuration -(defgroup mu4e-search nil - "Search-related settings." - :group 'mu4e) - -(defcustom mu4e-search-results-limit 500 - "Maximum number of results to show. -This affects performance, especially when -`mu4e-summary-include-related' is non-nil. -Set to -1 for no limits." - :type '(choice (const :tag "Unlimited" -1) - (integer :tag "Limit")) - :group 'mu4e-search) -(define-obsolete-variable-alias 'mu4e-headers-results-limit - 'mu4e-search-results-limit "1.7.0") - -(defvar mu4e-search-full nil - "Whether to search for all results. -If this is nil, search for up to `mu4e-search-results-limit')") - -(define-obsolete-variable-alias 'mu4e-headers-full-search - 'mu4e-search-full "1.7.0") - -(defvar mu4e-search-threads t - "Whether to calculate threads for the search results.") -(define-obsolete-variable-alias 'mu4e-headers-show-threads - 'mu4e-search-threads "1.7.0") - -(defcustom mu4e-search-query-rewrite-function 'identity - "Function to rewrite a query. - -It takes a search expression string, and returns a possibly - changed search expression string. - -This function is applied on the search expression just before -searching, and allows users to modify the query. - -For instance, we could change and of workmail into -\"maildir:/long-path-to-work-related-emails\", by setting the function - -(setq mu4e-query-rewrite-function - (lambda(expr) - (replace-regexp-in-string \"workmail\" - \"maildir:/long-path-to-work-related-emails\" expr))) - -It is good to remember that the replacement does not understand -anything about the query, it just does text replacement." - :type 'function - :group 'mu4e-search) - -(define-obsolete-variable-alias 'mu4e-query-rewrite-function - 'mu4e-search-query-rewrite-function "1.7.0") - -(defcustom mu4e-search-bookmark-hook nil - "Hook run just after invoking a bookmarked search. - -This function receives the query as its parameter, before any -rewriting as per `mu4e-query-rewrite-function' has taken place. - -The reason to use this instead of `mu4e-headers-search-hook' is -if you only want to execute a hook when a search is entered via a -bookmark, e.g. if you'd like to treat the bookmarks as a custom -folder and change the options for the search." - :type 'hook - :group 'mu4e-search) - -(define-obsolete-variable-alias - 'mu4e-headers-search-bookmark-hook - 'mu4e-search-bookmark-hook "1.7.0") - -(defcustom mu4e-search-hook nil - "Hook run just before executing a new search operation. -This function receives the query as its parameter, before any -rewriting as per `mu4e-query-rewrite-function' has taken place - -This is a more general hook facility than the -`mu4e-search-bookmark-hook'. It gets called on every -executed search, not just those that are invoked via bookmarks, -but also manually invoked searches." - :type 'hook - :group 'mu4e-search) - -(define-obsolete-variable-alias 'mu4e-headers-search-hook - 'mu4e-search-hook "1.7.0") - -;;; Interactive functions - -(defun mu4e-search (&optional expr prompt edit ignore-history msgid show) - "Search for query EXPR. - -Switch to the output buffer for the results. This is an -interactive function which ask user for EXPR. PROMPT, if non-nil, -is the prompt used by this function (default is \"Search for:\"). -If EDIT is non-nil, instead of executing the query for EXPR, let -the user edit the query before executing it. - -If IGNORE-HISTORY is true, do *not* update the query history -stack. If MSGID is non-nil, attempt to move point to the first -message with that message-id after searching. If SHOW is non-nil, -show the message with MSGID." - (interactive) - (let* ((prompt (mu4e-format (or prompt "Search for: "))) - (expr - (if (or (null expr) edit) - (mu4e-read-query prompt expr) - expr))) - (mu4e-mark-handle-when-leaving) - (mu4e--search-execute expr ignore-history) - (setq mu4e~headers-msgid-target msgid - mu4e~headers-view-target show))) - -(define-obsolete-function-alias 'mu4e-headers-search 'mu4e-search "1.7.0") - -(defun mu4e-search-edit () - "Edit the last search expression." - (interactive) - (mu4e-search mu4e--search-last-query nil t)) - -(define-obsolete-variable-alias 'mu4e-headers-search-edit - 'mu4e-search-edit "1.7.0") - -(defun mu4e-search-bookmark (&optional expr edit) - "Search using some bookmarked query EXPR. -If EDIT is non-nil, let the user edit the bookmark before starting -the search." - (interactive) - (let ((expr - (or expr - (mu4e-ask-bookmark (if edit "Select bookmark: " "Bookmark: "))))) - (run-hook-with-args 'mu4e-search-bookmark-hook expr) - (mu4e-search expr (when edit "Edit bookmark: ") edit))) - -(define-obsolete-function-alias 'mu4e-headers-search-bookmark - 'mu4e-search-bookmark "1.7.0") - -(defun mu4e-search-bookmark-edit () - "Edit an existing bookmark before executing it." - (interactive) - (mu4e-search-bookmark nil t)) - -(define-obsolete-function-alias 'mu4e-headers-search-bookmark-edit - 'mu4e-search-bookmark-edit "1.7.0") - -(defun mu4e-search-narrow(&optional filter) - "Narrow the last search. -Do so by appending search expression FILTER to the last search -expression. Note that you can go back to previous -query (effectively, 'widen' it), with `mu4e-search-prev'." - (interactive - (let ((filter - (read-string (mu4e-format "Narrow down to: ") - nil 'mu4e~headers-search-hist nil t))) - (list filter))) - (unless mu4e--search-last-query - (mu4e-warn "There's nothing to filter")) - (mu4e-headers-search - (format "(%s) AND (%s)" mu4e--search-last-query filter))) - -(define-obsolete-function-alias 'mu4e-headers-search-narrow - 'mu4e-search-narrow "1.7.0") - -;; (defun mu4e-headers-change-sorting (&optional field dir) -;; "Change the sorting/threading parameters. -;; FIELD is the field to sort by; DIR is a symbol: either 'ascending, -;; 'descending, 't (meaning: if FIELD is the same as the current -;; sortfield, change the sort-order) or nil (ask the user)." -;; (interactive) -;; (let* ((field -;; (or field -;; (mu4e-read-option "Sortfield: " mu4e~headers-sort-field-choices))) -;; ;; note: 'sortable' is either a boolean (meaning: if non-nil, this is -;; ;; sortable field), _or_ another field (meaning: sort by this other field). -;; (sortable (plist-get (cdr (assoc field mu4e-header-info)) :sortable)) -;; ;; error check -;; (sortable -;; (if sortable -;; sortable -;; (mu4e-error "Not a sortable field"))) -;; (sortfield (if (booleanp sortable) field sortable)) -;; (dir -;; (cl-case dir -;; ((ascending descending) dir) -;; ;; change the sort order if field = curfield -;; (t -;; (if (eq sortfield mu4e-headers-sort-field) -;; (if (eq mu4e-headers-sort-direction 'ascending) -;; 'descending 'ascending) -;; 'descending)) -;; (mu4e-read-option "Direction: " -;; '(("ascending" . 'ascending) ("descending" . 'descending)))))) -;; (setq -;; mu4e-headers-sort-field sortfield -;; mu4e-headers-sort-direction dir) -;; (mu4e-message "Sorting by %s (%s)" -;; (symbol-name sortfield) -;; (symbol-name mu4e-headers-sort-direction)) -;; (mu4e-headers-rerun-search))) - -;; (defun mu4e~headers-toggle (name togglevar dont-refresh) -;; "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil, -;; re-run the last search." -;; (set togglevar (not (symbol-value togglevar))) -;; (mu4e-message "%s turned %s%s" -;; name -;; (if (symbol-value togglevar) "on" "off") -;; (if dont-refresh -;; " (press 'g' to refresh)" "")) -;; (unless dont-refresh -;; (mu4e-headers-rerun-search))) - -;; (defun mu4e-headers-toggle-threading (&optional dont-refresh) -;; "Toggle `mu4e-headers-show-threads'. With prefix-argument, do -;; _not_ refresh the last search with the new setting for threading." -;; (interactive "P") -;; (mu4e~headers-toggle "Threading" 'mu4e-headers-show-threads dont-refresh)) - -;; (defun mu4e-headers-toggle-full-search (&optional dont-refresh) -;; "Toggle `mu4e-headers-full-search'. With prefix-argument, do -;; _not_ refresh the last search with the new setting for threading." -;; (interactive "P") -;; (mu4e~headers-toggle "Full-search" -;; 'mu4e-headers-full-search dont-refresh)) - -;; (defun mu4e-headers-toggle-include-related (&optional dont-refresh) -;; "Toggle `mu4e-headers-include-related'. With prefix-argument, do -;; _not_ refresh the last search with the new setting for threading." -;; (interactive "P") -;; (mu4e~headers-toggle "Include-related" -;; 'mu4e-headers-include-related dont-refresh)) - -;; (defun mu4e-headers-toggle-skip-duplicates (&optional dont-refresh) -;; "Toggle `mu4e-headers-skip-duplicates'. With prefix-argument, do -;; _not_ refresh the last search with the new setting for threading." -;; (interactive "P") -;; (mu4e~headers-toggle "Skip-duplicates" -;; 'mu4e-headers-skip-duplicates dont-refresh)) - - -;;; History - -(defvar mu4e--search-last-query nil - "The present (most recent) query.") -(defvar mu4e--search-query-past nil - "Stack of queries before the present one.") -(defvar mu4e--search-query-future nil - "Stack of queries after the present one.") -(defvar mu4e--search-query-stack-size 20 - "Maximum size for the query stacks.") - -(defun mu4e--search-push-query (query where) - "Push QUERY to one of the query stacks. -WHERE is a symbol telling us where to push; it's a symbol, either -'future or 'past. Functional also removes duplicates, limits the -stack size." - (let ((stack - (cl-case where - (past mu4e--search-query-past) - (future mu4e--search-query-future)))) - ;; only add if not the same item - (unless (and stack (string= (car stack) query)) - (push query stack) - ;; limit the stack to `mu4e--search-query-stack-size' elements - (when (> (length stack) mu4e--search-query-stack-size) - (setq stack (cl-subseq stack 0 mu4e--search-query-stack-size))) - ;; remove all duplicates of the new element - (cl-remove-if (lambda (elm) (string= elm (car stack))) (cdr stack)) - ;; update the stacks - (cl-case where - (past (setq mu4e--search-query-past stack)) - (future (setq mu4e--search-query-future stack)))))) - -(defun mu4e--search-pop-query (whence) - "Pop a query from the stack. -WHENCE is a symbol telling us where to get it from, either `future' -or `past'." - (cl-case whence - (past - (unless mu4e--search-query-past - (mu4e-warn "No more previous queries")) - (pop mu4e--search-query-past)) - (future - (unless mu4e--search-query-future - (mu4e-warn "No more next queries")) - (pop mu4e--search-query-future)))) - - -(defun mu4e-search-rerun () - "Re-run the search for the last search expression." - (interactive) - ;; if possible, try to return to the same message - (let* ((msg (mu4e-message-at-point t)) - (msgid (and msg (mu4e-message-field msg :message-id)))) - (mu4e-headers-search mu4e~headers-last-query nil nil t msgid))) - -(define-obsolete-function-alias 'mu4e-headers-rerun-search - 'mu4e-search-rerun "1.7.0") - -(defun mu4e--search-query-navigate (whence) - "Execute the previous query from the query stacks. -WHENCE determines where the query is taken from and is a symbol, -either `future' or `past'." - (let ((query (mu4e--search-pop-query whence)) - (where (if (eq whence 'future) 'past 'future))) - (when query - (mu4e--search-push-query mu4e--search-last-query where) - (mu4e-search query nil nil t)))) - -(defun mu4e-search-next () - "Execute the next query from the query stack." - (interactive) - (mu4e--search-query-navigate 'future)) - -(define-obsolete-function-alias 'mu4e-headers-query-next - 'mu4e-search-next "1.7.0") - -(defun mu4e-search-prev () - "Execute the previous query from the query stacks." - (interactive) - (mu4e--search-query-navigate 'past)) - -(define-obsolete-function-alias 'mu4e-headers-query-prev - 'mu4e-search-prev "1.7.0") - -;; forget the past so we don't repeat it :/ -(defun mu4e-search-forget () - "Forget the search history." - (interactive) - (setq mu4e--search-query-past nil - mu4e--search-query-future nil) - (mu4e-message "Query history cleared")) - -(define-obsolete-function-alias 'mu4e-headers-forget-queries - 'mu4e-search-forget "1.7.0") - -(defun mu4e-last-query () - "Get the most recent query or nil if there is none." - mu4e--search-last-query) - -;;; Completion for queries - -(defvar mu4e--search-hist nil "History list of searches.") -(defvar mu4e-minibuffer-search-query-map - (let ((map (copy-keymap minibuffer-local-map))) - (define-key map (kbd "TAB") #'completion-at-point) - map) - "The keymap for reading a search query.") - -(defun mu4e-search-read-query (prompt &optional initial-input) - "Read a query with completion using PROMPT and INITIAL-INPUT." - (minibuffer-with-setup-hook - (lambda () - (setq-local completion-at-point-functions - #'mu4e--search-query-competion-at-point) - (use-local-map mu4e-minibuffer-search-query-map)) - (read-string prompt initial-input 'mu4e--search-hist))) - -(define-obsolete-function-alias 'mu4e-read-query - 'mu4e-search-read-query "1.7.0") - -(defconst mu4e--search-query-keywords - '("and" "or" "not" - "from:" "to:" "cc:" "bcc:" "contact:" "date:" "subject:" "body:" - "list:" "maildir:" "flag:" "mime:" "file:" "prio:" "tag:" "msgid:" - "size:" "embed:")) - -(defun mu4e--search-query-competion-at-point () - "Provide completion when entering search expressions." - (cond - ((not (looking-back "[:\"][^ \t]*" nil)) - (let ((bounds (bounds-of-thing-at-point 'word))) - (list (or (car bounds) (point)) - (or (cdr bounds) (point)) - mu4e--search-query-keywords))) - ((looking-back "flag:\\(\\w*\\)" nil) - (list (match-beginning 1) - (match-end 1) - '("attach" "draft" "flagged" "list" "new" "passed" "replied" - "seen" "trashed" "unread" "encrypted" "signed"))) - ;; ((looking-back "maildir:\\([a-zA-Z0-9/.]*\\)" nil) - ;; (list (match-beginning 1) - ;; (match-end 1) - ;; (mu4e-get-maildirs))) - ((looking-back "prio:\\(\\w*\\)" nil) - (list (match-beginning 1) - (match-end 1) - (list "high" "normal" "low"))) - ((looking-back "mime:\\([a-zA-Z0-9/-]*\\)" nil) - (list (match-beginning 1) - (match-end 1) - (mailcap-mime-types))))) - -(define-minor-mode mu4e-search-minor-mode - "Mode for searching for messages." - :global nil - :init-value nil ;; disabled by default - :group 'mu4e - :lighter "" - :keymap - (let ((map (make-sparse-keymap))) - (define-key map "s" 'mu4e-search) - (define-key map "S" 'mu4e-search-edit) - (define-key map "/" 'mu4e-search-narrow) - ;;(define-key map "j" 'mu4e~headers-jump-to-maildir) - (define-key map (kbd "") 'mu4e-search-prev) - (define-key map (kbd "\\") 'mu4e-search-prev) - (define-key map (kbd "") 'mu4e-search-next) - - (define-key map "b" 'mu4e-search-bookmark) - (define-key map "B" 'mu4e-search-bookmark-edit) - map)) - -(provide 'mu4e-search) -;;; mu4e-search.el ends here diff --git a/mu4e/mu4e-speedbar.el b/mu4e/mu4e-speedbar.el index c2c414e9..69248c3a 100644 --- a/mu4e/mu4e-speedbar.el +++ b/mu4e/mu4e-speedbar.el @@ -1,6 +1,6 @@ ;;; mu4e-speedbar --- Speedbar support for mu4e -*- lexical-binding: t -*- -;; Copyright (C) 2012-2021 Antono Vasiljev, Dirk-Jan C. Binnema +;; Copyright (C) 2012-2020 Antono Vasiljev, Dirk-Jan C. Binnema ;; Author: Antono Vasiljev ;; Version: 0.1 @@ -35,7 +35,7 @@ (require 'mu4e-vars) (require 'mu4e-headers) (require 'mu4e-context) -(require 'mu4e-bookmarks) +(require 'mu4e-utils) (defvar mu4e-main-speedbar-key-map nil "Keymap used when in mu4e display mode.") @@ -91,7 +91,8 @@ (defun mu4e~speedbar-maildir (&optional _text token _ident) "Jump to maildir TOKEN. TEXT and INDENT are not used." (dframe-with-attached-buffer - (mu4e-search (concat "\"maildir:" token "\"") current-prefix-arg))) + (mu4e-headers-search (concat "\"maildir:" token "\"") + current-prefix-arg))) (defun mu4e~speedbar-render-bookmark-list () "Insert the list of bookmarks in the speedbar" @@ -109,7 +110,7 @@ (defun mu4e~speedbar-bookmark (&optional _text token _ident) "Run bookmarked query TOKEN. TEXT and INDENT are not used." (dframe-with-attached-buffer - (mu4e-search token current-prefix-arg))) + (mu4e-headers-search token current-prefix-arg))) ;;;###autoload (defun mu4e-speedbar-buttons (&optional _buffer) diff --git a/mu4e/mu4e-update.el b/mu4e/mu4e-update.el deleted file mode 100644 index 82a56b15..00000000 --- a/mu4e/mu4e-update.el +++ /dev/null @@ -1,320 +0,0 @@ -;;; mu4e-update.el -- part of mu4e, -*- lexical-binding: t -*- - -;; Copyright (C) 2011-2020 Dirk-Jan C. Binnema - -;; Author: Dirk-Jan C. Binnema -;; Maintainer: 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 . - -;;; Commentary: - -;; Updating the mu4e message: calling a mail retrieval program -;; and re-running the index. - -;;; Code: - -(require 'mu4e-helpers) -(require 'mu4e-server) - -;;; Customization - -(defcustom mu4e-get-mail-command "true" - "Shell command to run to retrieve new mail. -Common values are \"offlineimap\", \"fetchmail\" or \"mbsync\", but -arbitrary shell-commands can be used. - -When set to the literal string \"true\" (the default), the -command simply finishes successfully (running the 'true' command) -without retrieving any mail. This can be useful when mail is -already retrieved in another way." - :type 'string - :group 'mu4e - :safe 'stringp) - -(defcustom mu4e-index-update-error-warning t - "Whether to display warnings during the retrieval process. -This depends on the `mu4e-get-mail-command' exit code." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-index-update-error-continue t - "Whether to continue with indexing after an error during retrieval." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-index-update-in-background t - "Whether to retrieve mail in the background." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-index-cleanup t - "Whether to run a cleanup phase after indexing. - -That is, validate that each message in the message store has a -corresponding message file in the filesystem. - -Having this option as t ensures that no non-existing messages are -shown but can slow with large message stores on slow file-systems." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-index-lazy-check nil - "Whether to only use a 'lazy check' during reindexing. -This influences how we decide whether a message -needs (re)indexing or not. - -When this is set to non-nil, mu only uses the directory -timestamps to decide whether it needs to check the messages -beneath it. This makes indexing much faster, but might miss some -changes. For this, you might want to occasionally call -`mu4e-update-index-nonlazy'." - :type 'boolean - :group 'mu4e - :safe 'booleanp) - -(defcustom mu4e-update-interval nil - "Number of seconds between mail retrieval/indexing. -If nil, don't update automatically. Note, changes in -`mu4e-update-interval' only take effect after restarting mu4e." - :type '(choice (const :tag "No automatic update" nil) - (integer :tag "Seconds")) - :group 'mu4e - :safe 'integerp) - -(defvar mu4e-update-pre-hook nil - "Hook run just *before* the mail-retrieval / database updating process starts. -You can use this hook for example to `mu4e-get-mail-command' with -some specific setting.") - -(defcustom mu4e-hide-index-messages nil - "Whether to hide the \"Indexing...\" and contacts messages." - :type 'boolean - :group 'mu4e) - - -(defvar mu4e-index-updated-hook nil - "Hook run when the indexing process had one or more updated messages. -This can be used as a simple way to invoke some action when new -messages appear, but note that an update in the index does not -necessarily mean a new message.") - - -(defvar mu4e-message-changed-hook nil - "Hook run when there is a message changed in db. -For new messages, it depends on `mu4e-index-updated-hook'. This -can be used as a simple way to invoke some action when a message -changed.") - -(make-obsolete-variable 'mu4e-msg-changed-hook - 'mu4e-message-changed-hook "0.9.19") - - - -;;; Internal variables -(defvar mu4e--progress-reporter nil - "Internal, the progress reporter object.") -(defvar mu4e--update-timer nil - "The mu4e update timer.") -(defconst mu4e--update-name " *mu4e-update*" - "Name of the process and buffer to update mail.") -(defconst mu4e--update-buffer-height 8 - "Height of the mu4e message retrieval/update buffer.") -(defvar mu4e--get-mail-ask-password "mu4e get-mail: Enter password: " - "Query string for `mu4e-get-mail-command' password.") -(defvar mu4e--get-mail-password-regexp "^Remote: Enter password: $" - "Regexp for a `mu4e-get-mail-command' password query.") - - -(defun mu4e--get-mail-process-filter (proc msg) - "Filter the MSG output of the `mu4e-get-mail-command' PROC. - -Currently the filter only checks if the command asks for a -password by matching the output against -`mu4e~get-mail-password-regexp'. The messages are inserted into -the process buffer. - -Also scrolls to the final line, and update the progress -throbber." - (when mu4e--progress-reporter - (progress-reporter-update mu4e--progress-reporter)) - - (when (string-match mu4e--get-mail-password-regexp msg) - (if (process-get proc 'x-interactive) - (process-send-string proc - (concat (read-passwd mu4e--get-mail-ask-password) - "\n")) - ;; TODO kill process? - (mu4e-error "Unrecognized password request"))) - (when (process-buffer proc) - (let ((inhibit-read-only t) - (procwin (get-buffer-window (process-buffer proc)))) - ;; Insert at end of buffer. Leave point alone. - (with-current-buffer (process-buffer proc) - (goto-char (point-max)) - (if (string-match ".*\r\\(.*\\)" msg) - (progn - ;; kill even with \r - (end-of-line) - (let ((end (point))) - (beginning-of-line) - (delete-region (point) end)) - (insert (match-string 1 msg))) - (insert msg))) - ;; Auto-scroll unless user is interacting with the window. - (when (and (window-live-p procwin) - (not (eq (selected-window) procwin))) - (with-selected-window procwin - (goto-char (point-max))))))) - - -(defun mu4e-index-message (frm &rest args) - "Display FRM with ARGS like `mu4e-message' for index messages. -However, if `mu4e-hide-index-messages' is non-nil, do not display anything." - (unless mu4e-hide-index-messages - (apply 'mu4e-message frm args))) - -(defun mu4e-update-index () - "Update the mu4e index." - (interactive) - (mu4e--server-index mu4e-index-cleanup mu4e-index-lazy-check)) - -(defun mu4e-update-index-nonlazy () - "Update the mu4e index non-lazily. -This is just a convenience wrapper for indexing the non-lazy way -if you otherwise want to use `mu4e-index-lazy-check'." - (interactive) - (let ((mu4e-index-cleanup t) (mu4e-index-lazy-check nil)) - (mu4e-update-index))) - -(defvar mu4e--update-buffer nil - "The buffer of the update process when updating.") - -(define-derived-mode mu4e--update-mail-mode -special-mode "mu4e:update" - "Major mode used for retrieving new e-mail messages in `mu4e'.") - -(define-key mu4e--update-mail-mode-map (kbd "q") 'mu4e-kill-update-mail) - -(defun mu4e--temp-window (buf height) - "Create a temporary window with HEIGHT at the bottom BUF." - (let ((win - (split-window - (frame-root-window) - (- (window-height (frame-root-window)) height)))) - (set-window-buffer win buf) - (set-window-dedicated-p win t) - win)) - -(defun mu4e--update-sentinel-func (proc _msg) - "Sentinel function for the update process PROC." - (when mu4e--progress-reporter - (progress-reporter-done mu4e--progress-reporter) - (setq mu4e--progress-reporter nil)) - (unless mu4e-hide-index-messages - (message nil)) - (if (or (not (eq (process-status proc) 'exit)) - (/= (process-exit-status proc) 0)) - (progn - (when mu4e-index-update-error-warning - (mu4e-message "Update process returned with non-zero exit code") - (sit-for 5)) - (when mu4e-index-update-error-continue - (mu4e-update-index))) - (mu4e-update-index)) - (when (buffer-live-p mu4e--update-buffer) - (unless (eq mu4e-split-view 'single-window) - (mapc #'delete-window (get-buffer-window-list mu4e--update-buffer))) - (kill-buffer mu4e--update-buffer))) - -;; complicated function, as it: -;; - needs to check for errors -;; - (optionally) pop-up a window -;; - (optionally) check password requests -(defun mu4e--update-mail-and-index-real (run-in-background) - "Get a new mail by running `mu4e-get-mail-command'. -If -RUN-IN-BACKGROUND is non-nil (or called with prefix-argument), -run in the background; otherwise, pop up a window." - (let* ((process-connection-type t) - (proc (start-process-shell-command - "mu4e-update" mu4e--update-name - mu4e-get-mail-command)) - (buf (process-buffer proc)) - (win (or run-in-background - (mu4e--temp-window buf mu4e--update-buffer-height)))) - (setq mu4e--update-buffer buf) - (when (window-live-p win) - (with-selected-window win - ;; ;;(switch-to-buffer buf) - ;; (set-window-dedicated-p win t) - (erase-buffer) - (insert "\n") ;; FIXME -- needed so output starts - (mu4e--update-mail-mode))) - (setq mu4e--progress-reporter - (unless mu4e-hide-index-messages - (make-progress-reporter - (mu4e-format "Retrieving mail...")))) - (set-process-sentinel proc 'mu4e--update-sentinel-func) - ;; if we're running in the foreground, handle password requests - (unless run-in-background - (process-put proc 'x-interactive (not run-in-background)) - (set-process-filter proc 'mu4e--get-mail-process-filter)))) - -(defun mu4e-update-mail-and-index (run-in-background) - "Get a new mail by running `mu4e-get-mail-command'. -If RUN-IN-BACKGROUND is non-nil (or called with prefix-argument), -run in the background; otherwise, pop up a window." - (interactive "P") - (unless mu4e-get-mail-command - (mu4e-error "`mu4e-get-mail-command' is not defined")) - (if (and (buffer-live-p mu4e--update-buffer) - (process-live-p (get-buffer-process mu4e--update-buffer))) - (mu4e-message "Update process is already running") - (progn - (run-hooks 'mu4e-update-pre-hook) - (mu4e--update-mail-and-index-real run-in-background)))) - -(defun mu4e-kill-update-mail () - "Stop the update process by killing it." - (interactive) - (let* ((proc (and (buffer-live-p mu4e--update-buffer) - (get-buffer-process mu4e--update-buffer)))) - (when (process-live-p proc) - (kill-process proc t)))) - -(define-obsolete-function-alias 'mu4e-interrupt-update-mail - 'mu4e-kill-update-mail "1.0-alpha0") - -(define-minor-mode mu4e-update-minor-mode - "Mode for triggering mu4e updates." - :global nil - :init-value nil ;; disabled by default - :group 'mu4e - :lighter "" - :keymap - (let ((map (make-sparse-keymap))) - (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index) - ;; for terminal users - (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index) - map)) - -(provide 'mu4e-update) -;;; mu4e-update.el ends here diff --git a/mu4e/mu4e-utils.el b/mu4e/mu4e-utils.el new file mode 100644 index 00000000..60c3eae9 --- /dev/null +++ b/mu4e/mu4e-utils.el @@ -0,0 +1,1392 @@ +;;; mu4e-utils.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 +;; Maintainer: 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 . + +;;; Commentary: + +;; Utility functions used in the mu4e + +;;; Code: + +(require 'org) +(require 'cl-lib) +(require 'cl-seq nil 'noerror) +(require 'mu4e-vars) +(require 'mu4e-message) +(require 'mu4e-meta) +(require 'mu4e-lists) +(require 'doc-view) + +;; keep the byte-compiler happy +(declare-function mu4e~proc-mkdir "mu4e-proc") +(declare-function mu4e~proc-ping "mu4e-proc") +(declare-function mu4e~proc-contacts "mu4e-proc") +(declare-function mu4e~proc-kill "mu4e-proc") +(declare-function mu4e~proc-index "mu4e-proc") +(declare-function mu4e~proc-add "mu4e-proc") +(declare-function mu4e~proc-mkdir "mu4e-proc") +(declare-function mu4e~proc-running-p "mu4e-proc") + +(declare-function mu4e-message-field-at-point "mu4e-proc") +(declare-function mu4e~proc-running-p "mu4e-proc") + +(declare-function mu4e~main-view "mu4e-main") + +(declare-function mu4e~context-autoswitch "mu4e-context") +(declare-function mu4e-context-determine "mu4e-context") +(declare-function mu4e-context-vars "mu4e-context") +(declare-function mu4e-context-current "mu4e-context") + +(declare-function show-all "org") + +;;; Various + +(defun mu4e-copy-message-path () + "Copy the message-path of message at point to the kill-ring." + (interactive) + (let ((path (mu4e-message-field-at-point :path))) + (kill-new path) + (mu4e-message "Saved '%s' to kill-ring" path))) + +(defun mu4e-personal-address-p (addr) + "Is ADDR a personal address? +Evaluate to nil if ADDR matches any of the personal addresses. +Uses (mu4e-personal-addresses) for the addresses with both the plain +addresses and /regular expressions/." + (when addr + (seq-find + (lambda (m) + (if (string-match "/\\(.*\\)/" m) + (let ((rx (match-string 1 m)) + (case-fold-search t)) + (if (string-match rx addr) t nil)) + (eq t (compare-strings addr nil nil m nil nil 'case-insensitive)))) + (mu4e-personal-addresses)))) + +(define-obsolete-function-alias 'mu4e-user-mail-address-p + 'mu4e-personal-address-p "1.5.5") + +(defmacro with~mu4e-context-vars (context &rest body) + "Evaluate BODY, with variables let-bound for CONTEXT (if any). +`funcall'." + (declare (indent 2)) + `(let* ((vars (and ,context (mu4e-context-vars ,context)))) + (cl-progv ;; XXX: perhaps use eval's lexical environment instead of progv? + (mapcar (lambda(cell) (car cell)) vars) + (mapcar (lambda(cell) (cdr cell)) vars) + (eval ,@body)))) + +;;; Folders (1/2) + +;; the standard folders can be functions too +(defun mu4e~get-folder (foldervar msg) + "Within the mu-context of MSG, get message folder FOLDERVAR. +If FOLDER is a string, return it, if it is a function, evaluate +this function with MSG as parameter (which may be `nil'), and +return the result." + (unless (member foldervar + '(mu4e-sent-folder mu4e-drafts-folder + mu4e-trash-folder mu4e-refile-folder)) + (mu4e-error "Folder must be one of mu4e-(sent|drafts|trash|refile)-folder")) + ;; get the value with the vars for the relevants context let-bound + (with~mu4e-context-vars (mu4e-context-determine msg nil) + (let* ((folder (symbol-value foldervar)) + (val + (cond + ((stringp folder) folder) + ((functionp folder) (funcall folder msg)) + (t (mu4e-error "unsupported type for %S" folder))))) + (or val (mu4e-error "%S evaluates to nil" foldervar))))) + +(defun mu4e-get-drafts-folder (&optional msg) + "Get the sent folder. See `mu4e-drafts-folder'." + (mu4e~get-folder 'mu4e-drafts-folder msg)) + +(defun mu4e-get-refile-folder (&optional msg) + "Get the folder for refiling. See `mu4e-refile-folder'." + (mu4e~get-folder 'mu4e-refile-folder msg)) + +(defun mu4e-get-sent-folder (&optional msg) + "Get the sent folder. See `mu4e-sent-folder'." + (mu4e~get-folder 'mu4e-sent-folder msg)) + +(defun mu4e-get-trash-folder (&optional msg) + "Get the sent folder. See `mu4e-trash-folder'." + (mu4e~get-folder 'mu4e-trash-folder msg)) + +;;; Self-destructing files + +(defun mu4e-remove-file-later (filename) + "Remove FILENAME in a few seconds." + (run-at-time "30 sec" nil + (lambda () (ignore-errors (delete-file filename))))) + +(defun mu4e-make-temp-file (ext) + "Create a temporary file with extension EXT. The file will +self-destruct in a few seconds, enough to open it in another +program." + (let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext)))) + (mu4e-remove-file-later tmpfile) + tmpfile)) + +;;; Folders (2/2) +;; +;; mu4e-attachment-dir is either a string or a function that takes a +;; filename and the mime-type as argument, either (or both) which can +;; be nil + +(defun mu4e~get-attachment-dir (&optional fname mimetype) + "Get the directory for saving attachments from +`mu4e-attachment-dir' (which can be either a string or a function, +see its docstring)." + (let + ((dir + (cond + ((stringp mu4e-attachment-dir) + mu4e-attachment-dir) + ((functionp mu4e-attachment-dir) + (funcall mu4e-attachment-dir fname mimetype)) + (t + (mu4e-error "unsupported type for mu4e-attachment-dir" ))))) + (if dir + (expand-file-name dir) + (mu4e-error "mu4e-attachment-dir evaluates to nil")))) + +;;; Maildir (1/2) + +(defun mu4e~guess-maildir (path) + "Guess the maildir for some path, or nil if cannot find it." + (let ((idx (string-match (mu4e-root-maildir) path))) + (when (and idx (zerop idx)) + (replace-regexp-in-string + (mu4e-root-maildir) + "" + (expand-file-name + (concat path "/../..")))))) + +(defun mu4e-create-maildir-maybe (dir) + "Offer to create maildir DIR if it does not exist yet. +Return t if the dir already existed, or an attempt has been made to +create it -- we cannot be sure creation succeeded here, since this +is done asynchronously. Otherwise, return nil. NOte, DIR has to be +an absolute path." + (if (and (file-exists-p dir) (not (file-directory-p dir))) + (mu4e-error "%s exists, but is not a directory." dir)) + (cond + ((file-directory-p dir) t) + ((yes-or-no-p (mu4e-format "%s does not exist yet. Create now?" dir)) + (mu4e~proc-mkdir dir) t) + (t nil))) + +;;; Messages, warnings and errors + +(defun mu4e-format (frm &rest args) + "Create [mu4e]-prefixed string based on format FRM and ARGS." + (concat + "[" (propertize "mu4e" 'face 'mu4e-title-face) "] " + (apply 'format frm + (mapcar (lambda (x) + (if (stringp x) + (decode-coding-string x 'utf-8) + x)) + args)))) + +(defun mu4e-message (frm &rest args) + "Like `message', but prefixed with mu4e. +If we're waiting for user-input or if there's some message in the +echo area, don't show anything." + (unless (or (active-minibuffer-window)) + (message "%s" (apply 'mu4e-format frm args)))) + +(defun mu4e-index-message (frm &rest args) + "Like `mu4e-message', but specifically for +index-messages. Doesn't display anything if +`mu4e-hide-index-messages' is non-nil. " + (unless mu4e-hide-index-messages + (apply 'mu4e-message frm args))) + +(defun mu4e-error (frm &rest args) + "Create [mu4e]-prefixed error based on format FRM and ARGS. +Does a local-exit and does not return, and raises a +debuggable (backtrace) error." + (mu4e-log 'error (apply 'mu4e-format frm args)) + (error "%s" (apply 'mu4e-format frm args))) + +;; the user-error function is only available in emacs-trunk +(unless (fboundp 'user-error) + (defalias 'user-error 'error)) + +(defun mu4e-warn (frm &rest args) + "Create [mu4e]-prefixed warning based on format FRM and ARGS. +Does a local-exit and does not return. In emacs versions below +24.2, the functions is the same as `mu4e-error'." + (mu4e-log 'error (apply 'mu4e-format frm args)) + (user-error "%s" (apply 'mu4e-format frm args))) + +;;; Reading user input + +(defun mu4e~read-char-choice (prompt choices) + "Read and return one of CHOICES, prompting for PROMPT. +Any input that is not one of CHOICES is ignored. This mu4e's +version of `read-char-choice', that becomes case-insentive after +trying an exact match." + (let ((choice) (chosen) (inhibit-quit nil)) + (while (not chosen) + (message nil);; this seems needed... + (setq choice (read-char-exclusive prompt)) + (if (eq choice 27) (keyboard-quit)) ;; quit if ESC is pressed + (setq chosen (or (member choice choices) + (member (downcase choice) choices) + (member (upcase choice) choices)))) + (car chosen))) + +(defun mu4e-read-option (prompt options) + "Ask user for an option from a list on the input area. +PROMPT describes a multiple-choice question to the user. +OPTIONS describe the options, and is a list of cells describing +particular options. Cells have the following structure: + + (OPTIONSTRING . RESULT) + +where OPTIONSTRING is a non-empty string describing the +option. The first character of OPTIONSTRING is used as the +shortcut, and obviously all shortcuts must be different, so you +can prefix the string with an uniquifying character. + +The options are provided as a list for the user to choose from; +user can then choose by typing CHAR. Example: + (mu4e-read-option \"Choose an animal: \" + '((\"Monkey\" . monkey) (\"Gnu\" . gnu) (\"xMoose\" . moose))) + +User now will be presented with a list: \"Choose an animal: + [M]onkey, [G]nu, [x]Moose\". + +Function will return the cdr of the list element." + (let* ((prompt (mu4e-format "%s" prompt)) + (optionsstr + (mapconcat + (lambda (option) + ;; try to detect old-style options, and warn + (when (characterp (car-safe (cdr-safe option))) + (mu4e-error + (concat "Please use the new format for options/actions; " + "see the manual"))) + (let ((kar (substring (car option) 0 1))) + (concat + "[" (propertize kar 'face 'mu4e-highlight-face) "]" + (substring (car option) 1)))) + options ", ")) + (response + (mu4e~read-char-choice + (concat prompt optionsstr + " [" (propertize "C-g" 'face 'mu4e-highlight-face) + " to cancel]") + ;; the allowable chars + (cl-map 'list (lambda(elm) (string-to-char (car elm))) options))) + (chosen + (cl-find-if + (lambda (option) (eq response (string-to-char (car option)))) + options))) + (if chosen + (cdr chosen) + (mu4e-warn "Unknown shortcut '%c'" response)))) + +;;; Maildir (1/2) + +(defun mu4e~get-maildirs-1 (path mdir) + "Get maildirs under path, recursively, as a list of relative paths." + (let ((dirs) + (dentries + (ignore-errors + (directory-files-and-attributes + (concat path mdir) nil + "^[^.]\\|\\.[^.][^.]" t)))) + (dolist (dentry dentries) + (when (and (booleanp (cadr dentry)) (cadr dentry)) + (if (file-accessible-directory-p + (concat (mu4e-root-maildir) "/" mdir "/" (car dentry) "/cur")) + (setq dirs (cons (concat mdir (car dentry)) dirs))) + (unless (member (car dentry) '("cur" "new" "tmp")) + (setq dirs (append dirs (mu4e~get-maildirs-1 path + (concat mdir (car dentry) "/"))))))) + dirs)) + +(defvar mu4e-cache-maildir-list nil + "Whether to cache the list of maildirs; set it to t if you find +that generating the list on the fly is too slow. If you do, you +can set `(mu4e-root-maildir)-list' to nil to force regenerating the +cache the next time `mu4e-get-maildirs' gets called.") + +(defvar mu4e-maildir-list nil + "Cached list of maildirs.") + +(defun mu4e-get-maildirs () + "Get maildirs under `mu4e-maildir', recursively, as a list of +relative paths (ie., /archive, /sent etc.). Most of the work is +done in `mu4e~get-maildirs-1'. Note, these results are /cached/ +if `mu4e-cache-maildir-list' is customized to non-nil. In that case, +the list of maildirs will not change until you restart mu4e." + (unless (and mu4e-maildir-list mu4e-cache-maildir-list) + (setq mu4e-maildir-list + (sort + (append + (when (file-accessible-directory-p + (concat (mu4e-root-maildir) "/cur")) '("/")) + (mu4e~get-maildirs-1 (mu4e-root-maildir) "/")) + (lambda (s1 s2) (string< (downcase s1) (downcase s2)))))) + mu4e-maildir-list) + +(defun mu4e-ask-maildir (prompt) + "Ask the user for a shortcut (using PROMPT) as per +(mu4e-maildir-shortcuts), then return the corresponding folder +name. If the special shortcut 'o' (for _o_ther) is used, or if +`(mu4e-maildir-shortcuts)' evaluates to nil, let user choose from +all maildirs under `mu4e-maildir'." + (let ((prompt (mu4e-format "%s" prompt))) + (if (not (mu4e-maildir-shortcuts)) + (substring-no-properties + (funcall mu4e-completing-read-function prompt (mu4e-get-maildirs))) + (let* ((mlist (append (mu4e-maildir-shortcuts) + '((:maildir "ther" :key ?o)))) + (fnames + (mapconcat + (lambda (item) + (concat + "[" + (propertize (make-string 1 (plist-get item :key)) + 'face 'mu4e-highlight-face) + "]" + (plist-get item :maildir))) + mlist ", ")) + (kar (read-char (concat prompt fnames)))) + (if (member kar '(?/ ?o)) ;; user chose 'other'? + (substring-no-properties + (funcall mu4e-completing-read-function prompt + (mu4e-get-maildirs) nil nil "/")) + (or (plist-get + (cl-find-if (lambda (item) (= kar (plist-get item :key))) + (mu4e-maildir-shortcuts)) :maildir) + (mu4e-warn "Unknown shortcut '%c'" kar))))))) + +(defun mu4e-ask-maildir-check-exists (prompt) + "Like `mu4e-ask-maildir', but check for existence of the maildir, +and offer to create it if it does not exist yet." + (let* ((mdir (mu4e-ask-maildir prompt)) + (fullpath (concat (mu4e-root-maildir) mdir))) + (unless (file-directory-p fullpath) + (and (yes-or-no-p + (mu4e-format "%s does not exist. Create now?" fullpath)) + (mu4e~proc-mkdir fullpath))) + mdir)) + +;;; Bookmarks + (defun mu4e-ask-bookmark (prompt) + "Ask the user for a bookmark (using PROMPT) as defined in +`mu4e-bookmarks', then return the corresponding query." + (unless (mu4e-bookmarks) (mu4e-error "No bookmarks defined")) + (let* ((prompt (mu4e-format "%s" prompt)) + (bmarks + (mapconcat + (lambda (bm) + (concat + "[" (propertize (make-string 1 (plist-get bm :key)) + 'face 'mu4e-highlight-face) + "]" + (plist-get bm :name))) (mu4e-bookmarks) ", ")) + (kar (read-char (concat prompt bmarks)))) + (mu4e-get-bookmark-query kar))) + +(defun mu4e-get-bookmark-query (kar) + "Get the corresponding bookmarked query for shortcut character +KAR, or raise an error if none is found." + (let* ((chosen-bm + (or (cl-find-if + (lambda (bm) + (= kar (plist-get bm :key))) + (mu4e-bookmarks)) + (mu4e-warn "Unknown shortcut '%c'" kar))) + (expr (plist-get chosen-bm :query)) + (expr (if (not (functionp expr)) expr + (funcall expr))) + (query (eval expr))) + (if (stringp query) + query + (mu4e-warn "Expression must evaluate to query string ('%S')" expr)))) + + +(defun mu4e-bookmark-define (query name key) + "Define a bookmark for QUERY with name NAME and +shortcut-character KEY in the list of `mu4e-bookmarks'. This +replaces any existing bookmark with KEY." + (setq mu4e-bookmarks + (cl-remove-if + (lambda (bm) + (= (plist-get bm :key) key)) + (mu4e-bookmarks))) + (cl-pushnew `(:name ,name + :query ,query + :key ,key) + mu4e-bookmarks :test 'equal)) + + +;;; Converting flags->string and vice-versa + +(defun mu4e~flags-to-string-raw (flags) + "Convert a list of flags into a string as seen in Maildir +message files; flags are symbols draft, flagged, new, passed, +replied, seen, trashed and the string is the concatenation of the +uppercased first letters of these flags, as per [1]. Other flags +than the ones listed here are ignored. +Also see `mu4e-flags-to-string'. +\[1\]: http://cr.yp.to/proto/maildir.html" + (when flags + (let ((kar (cl-case (car flags) + ('draft ?D) + ('flagged ?F) + ('new ?N) + ('passed ?P) + ('replied ?R) + ('seen ?S) + ('trashed ?T) + ('attach ?a) + ('encrypted ?x) + ('signed ?s) + ('unread ?u)))) + (concat (and kar (string kar)) + (mu4e~flags-to-string-raw (cdr flags)))))) + +(defun mu4e-flags-to-string (flags) + "Remove duplicates and sort the output of `mu4e~flags-to-string-raw'." + (concat + (sort (cl-remove-duplicates + (append (mu4e~flags-to-string-raw flags) nil)) '>))) + +(defun mu4e~string-to-flags-1 (str) + "Convert a string with message flags as seen in Maildir +messages into a list of flags in; flags are symbols draft, +flagged, new, passed, replied, seen, trashed and the string is +the concatenation of the uppercased first letters of these flags, +as per [1]. Other letters than the ones listed here are ignored. +Also see `mu4e-flags-to-string'. +\[1\]: http://cr.yp.to/proto/maildir.html." + (when (/= 0 (length str)) + (let ((flag + (cl-case (string-to-char str) + (?D 'draft) + (?F 'flagged) + (?P 'passed) + (?R 'replied) + (?S 'seen) + (?T 'trashed)))) + (append (when flag (list flag)) + (mu4e~string-to-flags-1 (substring str 1)))))) + +(defun mu4e-string-to-flags (str) + "Convert a string with message flags as seen in Maildir messages +into a list of flags in; flags are symbols draft, flagged, new, +passed, replied, seen, trashed and the string is the concatenation +of the uppercased first letters of these flags, as per [1]. Other +letters than the ones listed here are ignored. Also see +`mu4e-flags-to-string'. \[1\]: +http://cr.yp.to/proto/maildir.html " + ;; "Remove duplicates from the output of `mu4e~string-to-flags-1'" + (cl-remove-duplicates (mu4e~string-to-flags-1 str))) + +;;; Various + +(defun mu4e-display-size (size) + "Get a string representation of SIZE (in bytes)." + (cond + ((>= size 1000000) (format "%2.1fM" (/ size 1000000.0))) + ((and (>= size 1000) (< size 1000000)) + (format "%2.1fK" (/ size 1000.0))) + ((< size 1000) (format "%d" size)) + (t (propertize "?" 'face 'mu4e-system-face)))) + + +(defun mu4e-display-manual () + "Display the mu4e manual page for the current mode. +Or go to the top level if there is none." + (interactive) + (info (cl-case major-mode + ('mu4e-main-mode "(mu4e)Main view") + ('mu4e-headers-mode "(mu4e)Headers view") + ('mu4e-view-mode "(mu4e)Message view") + (t "mu4e")))) + +;;; Misc + +(defun mu4e-last-query () + "Get the most recent query or nil if there is none." + (when (buffer-live-p (mu4e-get-headers-buffer)) + (with-current-buffer (mu4e-get-headers-buffer) + mu4e~headers-last-query))) + +(defvar gnus-article-buffer) ;; Fix byte-compiler warning. +(defun mu4e-get-view-buffer () + "Get the view buffer, if any." + (get-buffer + (if mu4e-view-use-old + mu4e~view-buffer-name + gnus-article-buffer))) + +(defun mu4e-get-headers-buffer () + (get-buffer mu4e~headers-buffer-name)) + +(defun mu4e-select-other-view () + "When the headers view is selected, select the message view (if +that has a live window), and vice versa." + (interactive) + (let* ((other-buf + (cond + ((eq major-mode 'mu4e-headers-mode) + (mu4e-get-view-buffer)) + ((eq major-mode 'mu4e-view-mode) + (mu4e-get-headers-buffer)))) + (other-win (and other-buf (get-buffer-window other-buf)))) + (if (window-live-p other-win) + (select-window other-win) + (mu4e-message "No window to switch to")))) + + +(defconst mu4e-output-buffer-name "*mu4e-output*" + "*internal* Name of the mu4e output buffer.") + +(defun mu4e-process-file-through-pipe (path pipecmd) + "Process file at PATH through a pipe with PIPECMD." + (let ((buf (get-buffer-create mu4e-output-buffer-name))) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (erase-buffer) + (call-process-shell-command pipecmd path t t) + (view-mode))) + (switch-to-buffer buf))) + +(defvar mu4e~lists-hash nil + "Hashtable of mailing-list-id => shortname, based on + `mu4e~mailing-lists' and `mu4e-user-mailing-lists'.") + +(defun mu4e-get-mailing-list-shortname (list-id) + "Get the shortname for a mailing-list with list-id LIST-ID. based +on `mu4e~mailing-lists', `mu4e-user-mailing-lists', and +`mu4e-mailing-list-patterns'." + (unless mu4e~lists-hash + (setq mu4e~lists-hash (make-hash-table :test 'equal)) + (dolist (cell mu4e~mailing-lists) + (puthash (car cell) (cdr cell) mu4e~lists-hash)) + (dolist (cell mu4e-user-mailing-lists) + (puthash (car cell) (cdr cell) mu4e~lists-hash))) + (or + (gethash list-id mu4e~lists-hash) + (and (boundp 'mu4e-mailing-list-patterns) + (cl-member-if + (lambda (pattern) + (string-match pattern list-id)) + mu4e-mailing-list-patterns) + (match-string 1 list-id)) + ;; if it's not in the db, take the part until the first dot if there is one; + ;; otherwise just return the whole thing + (if (string-match "\\([^.]*\\)\\." list-id) + (match-string 1 list-id) + list-id))) + +(defvar mu4e-index-updated-hook nil + "Hook run when the indexing process had one or more updated messages. +This can be used as a simple way to invoke some action when new +messages appear, but note that an update in the index does not +necessarily mean a new message.") + +(defvar mu4e-message-changed-hook nil + "Hook run when there is a message changed in db. For new +messages, it depends on `mu4e-index-updated-hook'. This can be +used as a simple way to invoke some action when a message +changed.") + +(make-obsolete-variable 'mu4e-msg-changed-hook + 'mu4e-message-changed-hook "0.9.19") + +(defvar mu4e~contacts-tstamp "0" + "Timestamp for the most recent contacts update." ) + +;;; Some handler functions for server messages + +(defun mu4e-info-handler (info) + "Handler function for (:info ...) sexps received from the server +process." + (let* ((type (plist-get info :info)) + (processed (plist-get info :processed)) + (updated (plist-get info :updated)) + (cleaned-up (plist-get info :cleaned-up)) + (mainbuf (get-buffer mu4e-main-buffer-name))) + (cond + ((eq type 'add) t) ;; do nothing + ((eq type 'index) + (if (eq (plist-get info :status) 'running) + (mu4e-index-message + "Indexing... processed %d, updated %d" processed updated) + (progn + (mu4e-index-message + "%s completed; processed %d, updated %d, cleaned-up %d" + (if mu4e-index-lazy-check "Lazy indexing" "Indexing") + processed updated cleaned-up) + ;; call the updated hook if anything changed. + (unless (zerop (+ updated cleaned-up)) + (run-hooks 'mu4e-index-updated-hook)) + (unless (and (not (string= mu4e~contacts-tstamp "0")) + (zerop (plist-get info :updated))) + (mu4e~request-contacts-maybe)) + (when (and (buffer-live-p mainbuf) (get-buffer-window mainbuf)) + (save-window-excursion + (select-window (get-buffer-window mainbuf)) + (mu4e~main-view 'refresh)))))) + ((plist-get info :message) + (mu4e-index-message "%s" (plist-get info :message)))))) + +(defun mu4e-error-handler (errcode errmsg) + "Handler function for showing an error." + ;; don't use mu4e-error here; it's running in the process filter context + (cl-case errcode + (4 (user-error "No matches for this search query.")) + (110 (display-warning 'mu4e errmsg :error)) ;; schema version. + (t (error "Error %d: %s" errcode errmsg)))) + + +;;; Contacts + +(defun mu4e~update-contacts (contacts &optional tstamp) + "Receive a sorted list of CONTACTS. +Each of the contacts has the form + (FULL_EMAIL_ADDRESS . RANK) and fill `mu4e~contacts-hash' with +it, with each contact mapped to an integer for their ranking. + +This is used by the completion function in mu4e-compose." + ;; We have our nicely sorted list, map them to a list + ;; of increasing integers. We use that map in the composer + ;; to sort them there. It would have been so much easier if emacs + ;; allowed us to use the sorted-list as-is, but no such luck. + (let ((n 0)) + (unless mu4e~contacts-hash + (setq mu4e~contacts-hash (make-hash-table :test 'equal :weakness nil + :size (length contacts)))) + (dolist (contact contacts) + (cl-incf n) + (let* ((address (plist-get contact :address)) + (address + (if (functionp mu4e-contact-process-function) + (funcall mu4e-contact-process-function address) + address))) + (when address ;; note the explicit deccode; the strings we get are utf-8, + ;; but emacs doesn't know yet. + (puthash (decode-coding-string address 'utf-8) + (plist-get contact :rank) mu4e~contacts-hash)))) + + (setq mu4e~contacts-tstamp (or tstamp "0")) + + (unless (zerop n) + (mu4e-index-message "Contacts updated: %d; total %d" + n (hash-table-count mu4e~contacts-hash))))) + +(defun mu4e-contacts-info () + "Display information about the cache used for contacts +completion; for testing/debugging." + (interactive) + (with-current-buffer (get-buffer-create "*mu4e-contacts-info*") + (erase-buffer) + (insert (format "complete addresses: %s\n" + (if mu4e-compose-complete-addresses "yes" "no"))) + (insert (format "only personal addresses: %s\n" + (if mu4e-compose-complete-only-personal "yes" "no"))) + (insert (format "only addresses seen after: %s\n" + (or mu4e-compose-complete-only-after "no restrictions"))) + + (when mu4e~contacts-hash + (insert (format "number of contacts cached: %d\n\n" + (hash-table-count mu4e~contacts-hash))) + (let ((contacts)) + (maphash (lambda (addr rank) + (setq contacts (cons (cons rank addr) contacts))) + mu4e~contacts-hash) + (setq contacts (sort contacts + (lambda(cell1 cell2) (< (car cell1) (car cell2))))) + (dolist (contact contacts) + (insert (format "%s\n" (cdr contact)))))) + + (pop-to-buffer "*mu4e-contacts-info*"))) + +(defun mu4e~check-requirements () + "Check for the settings required for running mu4e." + (unless (>= emacs-major-version 25) + (mu4e-error "Emacs >= 25.x is required for mu4e")) + (when mu4e~server-props + (unless (string= (mu4e-server-version) mu4e-mu-version) + (mu4e-error "mu server has version %s, but we need %s" + (mu4e-server-version) mu4e-mu-version))) + (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary)) + (mu4e-error "Please set `mu4e-mu-binary' to the full path to the mu + binary.")) + (dolist (var '(mu4e-sent-folder mu4e-drafts-folder + mu4e-trash-folder)) + (unless (and (boundp var) (symbol-value var)) + (mu4e-error "Please set %S" var)) + (unless (functionp (symbol-value var)) ;; functions are okay, too + (let* ((dir (symbol-value var)) + (path (concat (mu4e-root-maildir) dir))) + (unless (string= (substring dir 0 1) "/") + (mu4e-error "%S must start with a '/'" dir)) + (unless (mu4e-create-maildir-maybe path) + (mu4e-error "%s (%S) does not exist" path var)))))) + +(defun mu4e-running-p () + "Whether mu4e is running. +Checks whether the server process is live." + (mu4e~proc-running-p)) + +;;; Starting / getting mail / updating the index + +(defvar mu4e~update-timer nil + "The mu4e update timer.") +(defconst mu4e~update-name " *mu4e-update*" + "Name of the process and buffer to update mail.") +(defconst mu4e~update-buffer-height 8 + "Height of the mu4e message retrieval/update buffer.") + +(defvar mu4e~get-mail-ask-password "mu4e get-mail: Enter password: " + "Query string for `mu4e-get-mail-command' password.") +(defvar mu4e~get-mail-password-regexp "^Remote: Enter password: $" + "Regexp to match a password query in the `mu4e-get-mail-command' output.") + +(defun mu4e~request-contacts-maybe () + "If `mu4e-compose-complete-addresses' is non-nil, get/update +the list of contacts we use for autocompletion; otherwise, do +nothing." + (when mu4e-compose-complete-addresses + (setq mu4e-contacts-func 'mu4e~update-contacts) + (mu4e~proc-contacts + mu4e-compose-complete-only-personal + mu4e-compose-complete-only-after + mu4e~contacts-tstamp))) + +(defun mu4e~pong-handler (data func) + "Handle 'pong' responses from the mu server." + (setq mu4e~server-props (plist-get data :props)) ;; save info from the server + (let ((doccount (plist-get mu4e~server-props :doccount))) + (mu4e~check-requirements) + (when func (funcall func)) + (when (zerop doccount) + (mu4e-message "Store is empty; (re)indexing. This may take a while.") ; + (mu4e-update-index)) + (when (and mu4e-update-interval (null mu4e~update-timer)) + (setq mu4e~update-timer + (run-at-time 0 mu4e-update-interval + (lambda () (mu4e-update-mail-and-index + mu4e-index-update-in-background))))))) + +(defun mu4e-last-query-results () + "Get the results (counts) of the last cached queries. + +The cached queries are the bookmark / maildir queries that are +used to populated the read/unread counts in the main view. They +are refreshed when calling `(mu4e)', i.e., when going to the main +view. + +The results are a list of elements of the form + (:query \"query string\" + :count + :unread )" + (plist-get mu4e~server-props :queries)) + + +(defun mu4e-last-query-result (query) + "Get the last result for some cached query, as per + `mu4e-bookmark-query-results' or nil if not found." + (cl-find-if + (lambda (elm) (string= (plist-get elm :query) query)) + (mu4e-last-query-results))) + + +(defun mu4e~start (&optional func) + "If `mu4e-contexts' have been defined, but we don't have a +context yet, switch to the matching one, or none matches, the +first. If mu4e is already running, execute function FUNC (if +non-nil). Otherwise, check various requireme`'nts, then start mu4e. +When successful, call FUNC (if non-nil) afterwards." + (unless (mu4e-context-current) + (mu4e~context-autoswitch nil mu4e-context-policy)) + (setq mu4e-pong-func (lambda (info) (mu4e~pong-handler info func))) + (mu4e~proc-ping + (mapcar ;; send it a list of queries we'd like to see read/unread info for + (lambda (bm) + (funcall (or mu4e-query-rewrite-function #'identity) + (plist-get bm :query))) + ;; exclude bookmarks that are not strings, and with certain flags + (seq-filter (lambda (bm) + (and (stringp (plist-get bm :query)) + (not (or (plist-get bm :hide) (plist-get bm :hide-unread))))) + (append (mu4e-bookmarks) + (mu4e~maildirs-with-query))))) + ;; maybe request the list of contacts, automatically refreshed after + ;; reindexing + (unless mu4e~contacts-hash (mu4e~request-contacts-maybe))) + +(defun mu4e-clear-caches () + "Clear any cached resources." + (setq + mu4e-maildir-list nil + mu4e~contacts-hash nil + mu4e~contacts-tstamp "0")) + +(defun mu4e~stop () + "Stop the mu4e session." + (when mu4e~update-timer + (cancel-timer mu4e~update-timer) + (setq mu4e~update-timer nil)) + (mu4e-clear-caches) + (mu4e~proc-kill) + ;; kill all mu4e buffers + (mapc + (lambda (buf) + ;; When using the Gnus-based viewer, the view buffer has the + ;; kill-buffer-hook function mu4e~view-kill-buffer-hook-fn which kills the + ;; mm-* buffers created by Gnus' article mode. Those have been returned by + ;; `buffer-list' but might already be deleted in case the view buffer has + ;; been killed first. So we need a `buffer-live-p' check here. + (when (buffer-live-p buf) + (with-current-buffer buf + (when (member major-mode + '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode)) + (kill-buffer))))) + (buffer-list))) + +(defun mu4e~maildirs-with-query () + "Return a copy of `mu4e-maildirs-shortcuts' with :query populated. + +This is meant to be the exact same data structure as +`mu4e-bookmarks'." + (cl-mapcar + (lambda (m) + (append + ;; we want to change the :maildir key to :name, and add a :query key + (list :name (plist-get m :maildir) + :query (format "maildir:\"%s\"" (plist-get m :maildir))) + ;; next we want to append any other keys to our previous list (e.g. :hide, + ;; :key, etc) but skipping :maildir (since it's renamed to :name) + (cl-loop for (key value) on m by 'cddr + when (not (equal key :maildir)) + append (list key value)))) + (mu4e-maildir-shortcuts))) + +(defun mu4e~longest-of-maildirs-and-bookmarks () + "Return the length of longest name of bookmarks and maildirs." + (cl-loop for b in (append (mu4e-bookmarks) + (mu4e~maildirs-with-query)) + maximize (string-width (plist-get b :name)))) + + +;;; Indexing & Updating + +(defvar mu4e~progress-reporter nil + "Internal, the progress reporter object.") + +(defun mu4e~get-mail-process-filter (proc msg) + "Filter the output of `mu4e-get-mail-command'. +Currently the filter only checks if the command asks for a password +by matching the output against `mu4e~get-mail-password-regexp'. +The messages are inserted into the process buffer. + +Also scrolls to the final line, and update the progress throbber." + (when mu4e~progress-reporter + (progress-reporter-update mu4e~progress-reporter)) + + (when (string-match mu4e~get-mail-password-regexp msg) + (if (process-get proc 'x-interactive) + (process-send-string proc + (concat (read-passwd mu4e~get-mail-ask-password) + "\n")) + ;; TODO kill process? + (mu4e-error "Unrecognized password request"))) + (when (process-buffer proc) + (let ((inhibit-read-only t) + (procwin (get-buffer-window (process-buffer proc)))) + ;; Insert at end of buffer. Leave point alone. + (with-current-buffer (process-buffer proc) + (goto-char (point-max)) + (if (string-match ".*\r\\(.*\\)" msg) + (progn + ;; kill even with \r + (end-of-line) + (let ((end (point))) + (beginning-of-line) + (delete-region (point) end)) + (insert (match-string 1 msg))) + (insert msg))) + ;; Auto-scroll unless user is interacting with the window. + (when (and (window-live-p procwin) + (not (eq (selected-window) procwin))) + (with-selected-window procwin + (goto-char (point-max))))))) + +(defun mu4e-update-index () + "Update the mu4e index." + (interactive) + (mu4e~proc-index mu4e-index-cleanup mu4e-index-lazy-check)) + +(defun mu4e-update-index-nonlazy () + "Update the mu4e index non-lazily. +This is just a convenience wrapper for indexing the non-lazy way +if you otherwise want to use `mu4e-index-lazy-check'." + (interactive) + (let ((mu4e-index-cleanup t) (mu4e-index-lazy-check nil)) + (mu4e-update-index))) + + + +(defvar mu4e~update-buffer nil + "Internal, store the buffer of the update process when + updating.") + +(define-derived-mode mu4e~update-mail-mode special-mode "mu4e:update" + "Major mode used for retrieving new e-mail messages in `mu4e'.") + +(define-key mu4e~update-mail-mode-map (kbd "q") 'mu4e-kill-update-mail) + +(defun mu4e~temp-window (buf height) + "Create a temporary window with HEIGHT at the bottom of the +frame to display buffer BUF." + (let ((win + (split-window + (frame-root-window) + (- (window-height (frame-root-window)) height)))) + (set-window-buffer win buf) + (set-window-dedicated-p win t) + win)) + +(defun mu4e~update-sentinel-func (proc _msg) + "Sentinel function for the update process." + (when mu4e~progress-reporter + (progress-reporter-done mu4e~progress-reporter) + (setq mu4e~progress-reporter nil)) + (unless mu4e-hide-index-messages + (message nil)) + (if (or (not (eq (process-status proc) 'exit)) + (/= (process-exit-status proc) 0)) + (progn + (when mu4e-index-update-error-warning + (mu4e-message "Update process returned with non-zero exit code") + (sit-for 5)) + (when mu4e-index-update-error-continue + (mu4e-update-index))) + (mu4e-update-index)) + (when (buffer-live-p mu4e~update-buffer) + (unless (eq mu4e-split-view 'single-window) + (mapc #'delete-window (get-buffer-window-list mu4e~update-buffer))) + (kill-buffer mu4e~update-buffer))) + +;; complicated function, as it: +;; - needs to check for errors +;; - (optionally) pop-up a window +;; - (optionally) check password requests +(defun mu4e~update-mail-and-index-real (run-in-background) + "Get a new mail by running `mu4e-get-mail-command'. If +RUN-IN-BACKGROUND is non-nil (or called with prefix-argument), +run in the background; otherwise, pop up a window." + (let* ((process-connection-type t) + (proc (start-process-shell-command + "mu4e-update" mu4e~update-name + mu4e-get-mail-command)) + (buf (process-buffer proc)) + (win (or run-in-background + (mu4e~temp-window buf mu4e~update-buffer-height)))) + (setq mu4e~update-buffer buf) + (when (window-live-p win) + (with-selected-window win + ;; ;;(switch-to-buffer buf) + ;; (set-window-dedicated-p win t) + (erase-buffer) + (insert "\n") ;; FIXME -- needed so output starts + (mu4e~update-mail-mode))) + (setq mu4e~progress-reporter + (unless mu4e-hide-index-messages + (make-progress-reporter + (mu4e-format "Retrieving mail...")))) + (set-process-sentinel proc 'mu4e~update-sentinel-func) + ;; if we're running in the foreground, handle password requests + (unless run-in-background + (process-put proc 'x-interactive (not run-in-background)) + (set-process-filter proc 'mu4e~get-mail-process-filter)))) + +(defun mu4e-update-mail-and-index (run-in-background) + "Get a new mail by running `mu4e-get-mail-command'. If +run-in-background is non-nil (or called with prefix-argument), run +in the background; otherwise, pop up a window." + (interactive "P") + (unless mu4e-get-mail-command + (mu4e-error "`mu4e-get-mail-command' is not defined")) + (if (and (buffer-live-p mu4e~update-buffer) + (process-live-p (get-buffer-process mu4e~update-buffer))) + (mu4e-message "Update process is already running") + (progn + (run-hooks 'mu4e-update-pre-hook) + (mu4e~update-mail-and-index-real run-in-background)))) + +(defun mu4e-kill-update-mail () + "Stop the update process by killing it." + (interactive) + (let* ((proc (and (buffer-live-p mu4e~update-buffer) + (get-buffer-process mu4e~update-buffer)))) + (when (process-live-p proc) + (kill-process proc t)))) + +(define-obsolete-function-alias 'mu4e-interrupt-update-mail + 'mu4e-kill-update-mail "1.0-alpha0") + + +;;; Logging / debugging + +(defconst mu4e~log-max-size 1000000 + "Max number of characters to keep around in the log buffer.") +(defconst mu4e~log-buffer-name "*mu4e-log*" + "*internal* Name of the logging buffer.") + +(defun mu4e~get-log-buffer () + "Fetch (and maybe create) the log buffer." + (unless (get-buffer mu4e~log-buffer-name) + (with-current-buffer (get-buffer-create mu4e~log-buffer-name) + (view-mode) + + (when (fboundp 'so-long-mode) + (unless (eq major-mode 'so-long-mode) + (eval '(so-long-mode)))) + + (setq buffer-undo-list t))) + mu4e~log-buffer-name) + +(defun mu4e-log (type frm &rest args) + "Write a message of TYPE with format-string FRM and ARGS in +*mu4e-log* buffer, if the variable mu4e-debug is non-nil. Type is +either 'to-server, 'from-server or 'misc. This function is meant for debugging." + (when mu4e-debug + (with-current-buffer (mu4e~get-log-buffer) + (let* ((inhibit-read-only t) + (tstamp (propertize (format-time-string "%Y-%m-%d %T.%3N" + (current-time)) + 'face 'font-lock-string-face)) + (msg-face + (cl-case type + (from-server 'font-lock-type-face) + (to-server 'font-lock-function-name-face) + (misc 'font-lock-variable-name-face) + (error 'font-lock-warning-face) + (otherwise (mu4e-error "Unsupported log type")))) + (msg (propertize (apply 'format frm args) 'face msg-face))) + (save-excursion + (goto-char (point-max)) + (insert tstamp + (cl-case type + (from-server " <- ") + (to-server " -> ") + (error " !! ") + (otherwise " ")) + msg "\n") + + ;; if `mu4e-log-max-lines is specified and exceeded, clearest the oldest + ;; lines + (when (> (buffer-size) mu4e~log-max-size) + (goto-char (- (buffer-size) mu4e~log-max-size)) + (beginning-of-line) + (delete-region (point-min) (point)))))))) + +(defun mu4e-toggle-logging () + "Toggle between enabling/disabling debug-mode (in debug-mode, +mu4e logs some of its internal workings to a log-buffer. See +`mu4e-visit-log'." + (interactive) + (mu4e-log 'misc "logging disabled") + (setq mu4e-debug (not mu4e-debug)) + (mu4e-message "debug logging has been %s" + (if mu4e-debug "enabled" "disabled")) + (mu4e-log 'misc "logging enabled")) + +(defun mu4e-show-log () + "Visit the mu4e debug log." + (interactive) + (unless mu4e-debug (mu4e-toggle-logging)) + (let ((buf (get-buffer mu4e~log-buffer-name))) + (unless (buffer-live-p buf) + (mu4e-warn "No debug log available")) + (switch-to-buffer buf))) + + +(defun mu4e-split-ranges-to-numbers (str n) + "Convert STR containing attachment numbers into a list of numbers. +STR is a string; N is the highest possible number in the list. +This includes expanding e.g. 3-5 into 3,4,5. If the letter +\"a\" ('all')) is given, that is expanded to a list with numbers [1..n]." + (let ((str-split (split-string str)) + beg end list) + (dolist (elem str-split list) + ;; special number "a" converts into all attachments 1-N. + (when (equal elem "a") + (setq elem (concat "1-" (int-to-string n)))) + (if (string-match "\\([0-9]+\\)-\\([0-9]+\\)" elem) + ;; we have found a range A-B, which needs converting + ;; into the numbers A, A+1, A+2, ... B. + (progn + (setq beg (string-to-number (match-string 1 elem)) + end (string-to-number (match-string 2 elem))) + (while (<= beg end) + (cl-pushnew beg list :test 'equal) + (setq beg (1+ beg)))) + ;; else just a number + (cl-pushnew (string-to-number elem) list :test 'equal))) + ;; Check that all numbers are valid. + (mapc + (lambda (x) + (cond + ((> x n) + (mu4e-warn "Attachment %d bigger than maximum (%d)" x n)) + ((< x 1) + (mu4e-warn "Attachment number must be greater than 0 (%d)" x)))) + list))) + +;;; Misc 2 + +(defvar mu4e-imagemagick-identify "identify" + "Name/path of the Imagemagick 'identify' program.") + +(defun mu4e~image-width-scale (width height max_width max_height) + "Returns a width to use for proportional image scaling +to satisfy both MAX_WIDTH and MAX_HEIGHT restrictions." + (floor + (if (<= width max_width) + (if (<= height max_height) + width ; both width and height ok, just return width + (* (/ max_height (float height)) width)) ; height is too large, scale width by hmax/h + (if (<= height max_height) + max_width ; width is too large, return max_width as scaling + (let ((width_heightscale (* (/ max_height (float height)) width))) + (min max_width width_heightscale)))))) ; both too large, return smallest width + +(defun mu4e-display-image (imgpath &optional maxwidth maxheight) + "Display image IMG at point; optionally specify MAXWIDTH and +MAXHEIGHT. Function tries to use imagemagick if available (ie., +emacs was compiled with imagemagick support); otherwise MAXWIDTH +and MAXHEIGHT are ignored." + (let* ((have-im (and (fboundp 'imagemagick-types) + (imagemagick-types))) ;; hmm, should check for specific type + (identify (and have-im maxwidth + (executable-find mu4e-imagemagick-identify))) + (props (and identify (mapcar 'string-to-number + (split-string (shell-command-to-string + (format "%s -format '%%w %%h' %s" + identify (shell-quote-argument imgpath))))))) + (width (and props (car props))) + (height (and props (car (cdr props)))) + + (img (if have-im + (create-image imgpath 'imagemagick nil + :width (mu4e~image-width-scale width height maxwidth maxheight)) + (create-image imgpath)))) + (when img + (save-excursion + (insert "\n") + (let ((size (image-size img))) ;; inspired by gnus.. + (insert-char ?\n + (max 0 (round (- (window-height) (or maxheight (cdr size)) 1) 2))) + (insert-char ?\. + (max 0 (round (- (window-width) (or maxwidth (car size))) 2))) + (insert-image img)))))) + + +(defun mu4e-hide-other-mu4e-buffers () + "Bury mu4e-buffers (main, headers, view) (and delete all windows +displaying it). Do _not_ bury the current buffer, though." + (interactive) + (unless (eq mu4e-split-view 'single-window) + (let ((curbuf (current-buffer))) + ;; note: 'walk-windows' does not seem to work correctly when modifying + ;; windows; therefore, the doloops here + (dolist (frame (frame-list)) + (dolist (win (window-list frame nil)) + (with-current-buffer (window-buffer win) + (unless (eq curbuf (current-buffer)) + (when (member major-mode '(mu4e-headers-mode mu4e-view-mode)) + (when (eq t (window-deletable-p win)) + (delete-window win))))))) t))) + + +(defun mu4e-get-time-date (prompt) + "Determine the emacs time value for the time/date entered by user + after PROMPT. Formats are all that are accepted by + `parse-time-string'." + (let ((timestr (read-string (mu4e-format "%s" prompt)))) + (apply 'encode-time (org-parse-time-string timestr)))) + + +;;; Mu4e-org-mode + +(define-derived-mode mu4e-org-mode org-mode "mu4e:org" + "Major mode for mu4e documents, derived from + `org-mode'.") + +(defun mu4e-info (path) + "Show a buffer with the information (an org-file) at PATH." + (unless (file-exists-p path) + (mu4e-error "Cannot find %s" path)) + (let ((curbuf (current-buffer))) + (find-file path) + (mu4e-org-mode) + (setq buffer-read-only t) + (define-key mu4e-org-mode-map (kbd "q") + `(lambda () + (interactive) + (bury-buffer) + (switch-to-buffer ,curbuf))))) + +(defun mu4e-about () + "Show the mu4e 'about' page." + (interactive) + (mu4e-info (concat mu4e-doc-dir "/mu4e-about.org"))) + +(defun mu4e-news () + "Show the mu4e 'about' page." + (interactive) + (mu4e-info (concat mu4e-doc-dir "/NEWS.org"))) + +;;; Misc 3 + +(defun mu4e-refresh-message (path) + "Re-parse message at PATH; if this works, we will +receive (:info add :path :docid ) as well as (:update +)." + (mu4e~proc-add path)) + + +(defun mu4e~fontify-cited () + "Colorize message content based on the citation level. This is +used in the view and compose modes." + (save-excursion + (goto-char (point-min)) + (when (search-forward-regexp "^\n" nil t) ;; search the first empty line + (while (re-search-forward mu4e-cited-regexp nil t) + (let* ((level (string-width (replace-regexp-in-string + "[^>]" "" (match-string 0)))) + (face (unless (zerop level) + (intern-soft (format "mu4e-cited-%d-face" level))))) + (when face + (add-text-properties (line-beginning-position 1) + (line-end-position 1) `(face ,face)))))))) + +(defun mu4e~fontify-signature () + "Give the message signatures a distinctive color. This is used in +the view and compose modes and will color each signature in digest messages adhering to RFC 1153." + (let ((inhibit-read-only t)) + (save-excursion + ;; give the footer a different color... + (goto-char (point-min)) + (while (re-search-forward "^-- *$" nil t) + (let ((p (point)) + (end (or + (re-search-forward "\\(^-\\{30\\}.*$\\)" nil t) ;; 30 by RFC1153 + (point-max)))) + (add-text-properties p end '(face mu4e-footer-face))))))) + +;;; Misc 4 + +(defun mu4e~quote-for-modeline (str) + "Quote a string to be used literally in the modeline. The +string will be shortened to fit if its length exceeds +`mu4e-modeline-max-width'." + (replace-regexp-in-string + "%" "%%" + (truncate-string-to-width str mu4e-modeline-max-width 0 nil t))) + +(defun mu4e~active-composition-buffers () + "Return all active mu4e composition buffers" + (let (buffers) + (save-excursion + (dolist (buffer (buffer-list t)) + (set-buffer buffer) + (when (eq major-mode 'mu4e-compose-mode) + (push (buffer-name buffer) buffers)))) + (nreverse buffers))) + + +;; +;; Loading messages +;; + +(defvar mu4e-loading-mode-map nil "Keymap for *mu4e-loading* buffers.") +(unless mu4e-loading-mode-map + (setq mu4e-loading-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "n" 'ignore) + (define-key map "p" 'ignore) + (define-key map "q" + (lambda()(interactive) + (if (eq mu4e-split-view 'single-window) + 'kill-buffer + 'kill-buffer-and-window))) + map))) +(fset 'mu4e-loading-mode-map mu4e-loading-mode-map) + +(define-derived-mode mu4e-loading-mode special-mode + "mu4e:loading" + (use-local-map mu4e-loading-mode-map) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (propertize "Loading message..." + 'face 'mu4e-system-face 'intangible t)))) + +;; +;; Bug Reference mode support +;; + +;; This is Emacs 28 stuff but there is no need to guard it with some (f)boundp +;; checks (which would return nil if bug-reference.el is not loaded before +;; mu4e) since the function definition doesn't hurt and `add-hook' works fine +;; for not yet defined variables (by creating them). +(declare-function bug-reference-maybe-setup-from-mail "ext:bug-reference") +(defun mu4e-view--try-setup-bug-reference-mode () + "Try to guess bug-reference setup from the current mu4e mail. +Looks at the maildir and the mail headers List, List-Id, Maildir, +To, From, Cc, and Subject and tries to guess suitable values for +`bug-reference-bug-regexp' and `bug-reference-url-format' by +matching the maildir name against GROUP-REGEXP and each header +value against HEADER-REGEXP in +`bug-reference-setup-from-mail-alist'." + (when (derived-mode-p 'mu4e-view-mode) + (let (header-values) + (save-excursion + (goto-char (point-min)) + (dolist (field '("list" "list-id" "to" "from" "cc" "subject")) + (let ((val (mail-fetch-field field))) + (when val + (push val header-values))))) + (bug-reference-maybe-setup-from-mail + (mail-fetch-field "maildir") + header-values)))) + +(add-hook 'bug-reference-auto-setup-functions + #'mu4e-view--try-setup-bug-reference-mode) + +;;; _ +(provide 'mu4e-utils) +;;; mu4e-utils.el ends here diff --git a/mu4e/mu4e-vars.el b/mu4e/mu4e-vars.el index 6a3df14d..db3a60a7 100644 --- a/mu4e/mu4e-vars.el +++ b/mu4e/mu4e-vars.el @@ -26,13 +26,139 @@ (require 'mu4e-meta) (require 'message) -(require 'mu4e-helpers) - -;;; Configuration + +(declare-function mu4e-error "mu4e-utils") + +;;; Customization + (defgroup mu4e nil - "Mu4e - an email-client for Emacs." + "mu4e - mu for emacs" :group 'mail) +(defcustom mu4e-mu-home nil + "Location of an alternate mu home dir. If not set, use the +defaults, based on the XDG Base Directory Specification." + :group 'mu4e + :type '(choice (const :tag "Default location" nil) + (directory :tag "Specify location")) + :safe 'stringp) + +(defcustom mu4e-mu-binary (executable-find "mu") + "Name of the mu-binary to use. +If it cannot be found in your PATH, you can specify the full +path." + :type 'file + :group 'mu4e + :safe 'stringp) + +(defcustom mu4e-mu-debug nil + "Whether to run the mu binary in debug-mode. +Setting this to t increases the amount of information in the log." + :type 'boolean + :group 'mu4e) + +(make-obsolete-variable 'mu4e-maildir + "determined by server; see `mu4e-root-maildir'." "1.3.8") + +(defcustom mu4e-org-support t + "Support org-mode links." + :type 'boolean + :group 'mu4e) + +(defgroup mu4e-view nil + "Settings for the message view." + :group 'mu4e) + +(defcustom mu4e-view-use-old nil + "If non-nil, use the old viewer. +Otherwise, use the new, Gnus-based viewer." + :type 'boolean + :group 'mu4e-view) + +(make-obsolete-variable 'mu4e-view-use-gnus 'mu4e-view-use-old "1.5.10") + +(defcustom mu4e-speedbar-support nil + "Support having a speedbar to navigate folders/bookmarks." + :type 'boolean + :group 'mu4e) + +(defcustom mu4e-get-mail-command "true" + "Shell command to run to retrieve new mail. +Common values are \"offlineimap\", \"fetchmail\" or \"mbsync\", but +arbitrary shell-commands can be used. + +When set to the literal string \"true\" (the default), the +command simply finishes successfully (running the 'true' command) +without retrieving any mail. This can be useful when mail is +already retrieved in another way." + :type 'string + :group 'mu4e + :safe 'stringp) + +(defcustom mu4e-index-update-error-warning t + "Whether to display warnings during the retrieval process. +This depends on the `mu4e-get-mail-command' exit code." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-update-error-continue t + "Whether to continue with indexing after an error during retrieval." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-update-in-background t + "Whether to retrieve mail in the background." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-cleanup t + "Whether to run a cleanup phase after indexing. + +That is, validate that each message in the message store has a +corresponding message file in the filesystem. + +Having this option as t ensures that no non-existing messages are +shown but can slow with large message stores on slow file-systems." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-index-lazy-check nil + "Whether to only use a 'lazy check' during reindexing. +This influences how we decide whether a message +needs (re)indexing or not. + +When this is set to non-nil, mu only uses the directory +timestamps to decide whether it needs to check the messages +beneath it. This makes indexing much faster, but might miss some +changes. For this, you might want to occasionally call +`mu4e-update-index-nonlazy'." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-update-interval nil + "Number of seconds between mail retrieval/indexing. +If nil, don't update automatically. Note, changes in +`mu4e-update-interval' only take effect after restarting mu4e." + :type '(choice (const :tag "No automatic update" nil) + (integer :tag "Seconds")) + :group 'mu4e + :safe 'integerp) + +(defvar mu4e-update-pre-hook nil + "Hook run just *before* the mail-retrieval / database updating process starts. +You can use this hook for example to `mu4e-get-mail-command' with +some specific setting.") + +(defcustom mu4e-hide-index-messages nil + "Whether to hide the \"Indexing...\" and contacts messages." + :type 'boolean + :group 'mu4e) + (defcustom mu4e-headers-include-related t "With this option set to non-nil, not just return the matches for a searches, but also messages that are related (through their @@ -49,13 +175,510 @@ and offlineimap." :type 'boolean :group 'mu4e-headers) +(defcustom mu4e-change-filenames-when-moving nil + "Change message file names when moving them. +When moving messages to different folders, normally mu/mu4e keep +the base filename the same (the flags-part of the filename may +change still). With this option set to non-nil, mu4e instead +changes the filename. This latter behavior works better with some +IMAP-synchronization programs such as mbsync; the default works +better with e.g. offlineimap." + :type 'boolean + :group 'mu4e + :safe 'booleanp) + +(defcustom mu4e-attachment-dir (expand-file-name "~/") + "Default directory for attaching and saving attachments. + +This can be either a string (a file system path), or a function +that takes a filename and the mime-type as arguments, and returns +the attachment dir. See Info node `(mu4e) Attachments' for +details. + +When this called for composing a message, both filename and +mime-type are nill." + :type 'directory + :group 'mu4e + :safe 'stringp) + +;; don't use the older vars anymore +(make-obsolete-variable 'mu4e-user-mail-address-regexp + 'mu4e-user-mail-address-list "0.9.9.x") +(make-obsolete-variable 'mu4e-my-email-addresses + 'mu4e-user-mail-address-list "0.9.9.x") +(make-obsolete-variable 'mu4e-user-mail-address-list + "determined by server; see `mu4e-personal-addresses'." "1.3.8") + +(defcustom mu4e-use-fancy-chars nil + "When set, allow fancy (Unicode) characters for marks/threads. +You can customize the exact fancy characters used with +`mu4e-marks' and various `mu4e-headers-..-mark' and +`mu4e-headers..-prefix' variables." + :type 'boolean + :group 'mu4e) + (defcustom mu4e-date-format-long "%c" "Date format to use in the message view. Follows the format of `format-time-string'." :type 'string :group 'mu4e) - +(defcustom mu4e-modeline-max-width 42 + "Determines the maximum length of the modeline string. +If the string exceeds this limit, it will be truncated to fit." + :type 'integer + :group 'mu4e) + +(defvar mu4e-debug nil + "When set to non-nil, log debug information to the *mu4e-log* buffer.") + +;; for backward compatibility, when a bookmark was defined with defstruct. +(cl-defun make-mu4e-bookmark (&key name query key) + "Create a mu4e proplist with the following elements: +- `name': the user-visible name of the bookmark +- `key': a single key to search for this bookmark +- `query': the query for this bookmark. Either a literal string or a function + that evaluates to a string." + `(:name ,name :query ,query :key ,key)) +(make-obsolete 'make-mu4e-bookmark "`unneeded; `mu4e-bookmarks' +are plists" "1.3.7") + +(defcustom mu4e-bookmarks + '(( :name "Unread messages" + :query "flag:unread AND NOT flag:trashed" + :key ?u) + ( :name "Today's messages" + :query "date:today..now" + :key ?t) + ( :name "Last 7 days" + :query "date:7d..now" + :hide-unread t + :key ?w) + ( :name "Messages with images" + :query "mime:image/*" + :key ?p)) + "List of pre-defined queries that are shown on the main screen. + +Each of the list elements is a plist with at least: +`:name' - the name of the query +`:query' - the query expression or function +`:key' - the shortcut key. + +Note that the :query parameter can be a function/lambda. + +Optionally, you can add the following: +`:hide' - if t, the bookmark is hidden from the main-view and + speedbar. +`:hide-unread' - do not show the counts of unread/total number + of matches for the query in the main-view. This can be useful +if a bookmark uses a very slow query. :hide-unread +is implied from :hide. Furthermore, it is implied if +`:query' is a function. + +Queries used to determine the unread/all counts do _not_ apply +`mu4e-query-rewrite-function'; nor do they discard duplicate or +unreadable messages (for efficiency). Thus, the numbers shown may +differ from the number you get from a 'real' query." + :type '(repeat (plist)) + :version "1.3.9" + :group 'mu4e) + +(defcustom mu4e-query-rewrite-function 'identity + "Function that takes a search expression string, and returns a + possibly changed search expression string. + +This function is applied on the search expression just before +searching, and allows users to modify the query. + +For instance, we could change and of workmail into +\"maildir:/long-path-to-work-related-emails\", by setting the function + +(setq mu4e-query-rewrite-function + (lambda(expr) + (replace-regexp-in-string \"workmail\" + \"maildir:/long-path-to-work-related-emails\" expr))) + +It is good to remember that the replacement does not understand +anything about the query, it just does text replacement." + :type 'function + :group 'mu4e) + +(defun mu4e-bookmarks () + "Get `mu4e-bookmarks' in the (new) format, converting from the +old format if needed." + (cl-map 'list + (lambda (item) + (if (and (listp item) (= (length item) 3)) + `(:name ,(nth 1 item) + :query ,(nth 0 item) + :key ,(nth 2 item)) + item)) + mu4e-bookmarks)) + + +(defcustom mu4e-split-view 'horizontal + "How to show messages / headers. +A symbol which is either: + * `horizontal': split horizontally (headers on top) + * `vertical': split vertically (headers on the left). + * `single-window': view and headers in one window (mu4e will try not to + touch your window layout), main view in minibuffer + * anything else: don't split (show either headers or messages, + not both) +Also see `mu4e-headers-visible-lines' +and `mu4e-headers-visible-columns'." + :type '(choice (const :tag "Split horizontally" horizontal) + (const :tag "Split vertically" vertical) + (const :tag "Single window" single-window) + (const :tag "Don't split" nil)) + :group 'mu4e-headers) + +(defcustom mu4e-view-max-specpdl-size 4096 + "The value of `max-specpdl-size' for displaying messages with Gnus." + :type 'integer + :group 'mu4e-view) + +(defcustom mu4e-view-show-images nil + "If non-nil, automatically display images in the view +buffer. Applies only to the _old_ message view." + :type 'boolean + :group 'mu4e-view) + +(defcustom mu4e-view-auto-mark-as-read t + "Automatically mark messages are 'read' when you read them. +This is the default behavior, but can be turned off, for example +when using a read-only file-system. + +This can also be set to a function; if so, receives a message +plist which should evaluate to nil if the message should *not* be +marked as read-only, or non-nil otherwise." + :type '(choice + boolean + function) + :group 'mu4e-view) + + +(defcustom mu4e-confirm-quit t + "Whether to confirm to quit mu4e." + :type 'boolean + :group 'mu4e) + +(defcustom mu4e-cited-regexp + "^\\(\\([[:alpha:]]+\\)\\|\\( *\\)\\)\\(\\(>+ ?\\)+\\)" + "Regex that determines whether a line is a citation. +This recognizes lines starting with numbers of '>' +and spaces as well as citations of the type \"John> ... \"." + :type 'string + :group 'mu4e) + +(defcustom mu4e-completing-read-function 'ido-completing-read + "Function to be used to receive user-input during completion. +This is used to receive the name of the maildir to switch to via +`mu4e~headers-jump-to-maildir'. + +Suggested possible values are: + * `completing-read': built-in completion method + * `ido-completing-read': dynamic completion within the minibuffer." + :type 'function + :options '(completing-read ido-completing-read) + :group 'mu4e) + +(defcustom mu4e-context-policy 'ask-if-none + "The policy to determine the context when entering the mu4e main view. + +If the value is `always-ask', ask the user unconditionally. + +In all other cases, if any context matches (using its match +function), this context is used. Otherwise, if none of the +contexts match, we have the following choices: + +- `pick-first': pick the first of the contexts available (ie. the default) +- `ask': ask the user +- `ask-if-none': ask if there is no context yet, otherwise leave it as it is +- nil: return nil; leaves the current context as is. + +Also see `mu4e-compose-context-policy'." + :type '(choice + (const :tag "Always ask what context to use, even if one matches" + always-ask) + (const :tag "Ask if none of the contexts match" ask) + (const :tag "Ask when there's no context yet" ask-if-none) + (const :tag "Pick the first context if none match" pick-first) + (const :tag "Don't change the context when none match" nil)) + :group 'mu4e) + +;;;; Crypto + +(defgroup mu4e-crypto nil + "Crypto-related settings." + :group 'mu4e) + +(make-obsolete-variable 'mu4e-auto-retrieve-keys "no longer used." "1.3.1") + +(defcustom mu4e-decryption-policy t + "Policy for dealing with encrypted parts. +The setting is a symbol: + * t: try to decrypt automatically + * `ask': ask before decrypting anything + * nil: don't try to decrypt anything. + +Note that this is not used unless `mu4e-view-use-old' is enabled." + :type '(choice (const :tag "Try to decrypt automatically" t) + (const :tag "Ask before decrypting anything" ask) + (const :tag "Don't try to decrypt anything" nil)) + :group 'mu4e-crypto) + +;;;; Address completion +;; +;; We put these options here rather than in mu4e-compose, because +;; mu4e-utils needs them. + +(defgroup mu4e-compose nil + "Message-composition related settings." + :group 'mu4e) + +(defcustom mu4e-compose-complete-addresses t + "Whether to do auto-completion of e-mail addresses." + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-complete-only-personal nil + "Whether to consider only 'personal' e-mail addresses for completion. +That is, addresses from messages where user was explicitly in one +of the address fields (this excludes mailing list messages). +These addresses are the ones specified with `mu init'." + :type 'boolean + :group 'mu4e-compose) + +(defcustom mu4e-compose-complete-only-after "2014-01-01" + "Consider only contacts last seen after this date. + +Date must be a string of the form YYY-MM-DD. + +This is useful for limiting a potentially enormous set of +contacts for auto-completion to just those that are present in +the e-mail corpus in recent timses. Set to nil to not have any +time-based restriction." + :type 'string + :group 'mu4e-compose) + +;; names and mail-addresses can be mapped onto their canonical +;; counterpart. use the customizeable function +;; mu4e-canonical-contact-function to do that. below the identity +;; function for mapping a contact onto the canonical one. +(defun mu4e-contact-identity (contact) + "Return the name and the mail-address of a CONTACT. +It is used as the identity function for converting contacts to +their canonical counterpart; useful as an example." + (let ((name (plist-get contact :name)) + (mail (plist-get contact :mail))) + (list :name name :mail mail))) + +(make-obsolete-variable 'mu4e-contact-rewrite-function + "mu4e-contact-process-function (see docstring)" "mu4e 1.3.2") +(make-obsolete-variable 'mu4e-compose-complete-ignore-address-regexp + "mu4e-contact-process-function (see docstring)" "mu4e 1.3.2") + +(defcustom mu4e-contact-process-function + (lambda(addr) ;; filter-out no-reply addresses + (unless (string-match-p "no[t]?[-\\.]?repl\\(y\\|ies\\)" addr) + addr)) + "Function for processing contact information for use in auto-completion. + +The function receives the contact as a string, e.g + \"Foo Bar \" + \"cuux@example.com\" + +The function should return either: +- nil: do not use this contact for completion +- the (possibly rewritten) address, which must be +an RFC-2822-compatible e-mail address." + :type 'function + :group 'mu4e-compose) + +(defcustom mu4e-compose-reply-ignore-address + '("no-?reply") + "Addresses to prune when doing wide replies. + +This can be a regexp matching the address, a list of regexps or a +predicate function. A value of nil keeps all the addresses." + :type '(choice + (const nil) + function + string + (repeat string)) + :group 'mu4e-compose) + +(defcustom mu4e-compose-reply-recipients 'ask + "Which recipients to use when replying to a message. +May be 'ask, 'all, 'sender. Note that that only applies to +non-mailing-list message; for those, mu4e always asks." + :type '(choice ask + all + sender) + :group 'mu4e-compose) + +(defcustom mu4e-compose-reply-to-address nil + "The Reply-To address. +Useful when this is not equal to the From: address." + :type 'string + :group 'mu4e-compose) + +(defcustom mu4e-compose-forward-as-attachment nil + "Whether to forward messages as attachments instead of inline." + :type 'boolean + :group 'mu4e-compose) + +;; backward compatibility +(make-obsolete-variable 'mu4e-reply-to-address + 'mu4e-compose-reply-to-address + "v0.9.9") + +(defcustom mu4e-compose-keep-self-cc nil + "When non-nil. keep your e-mail address in Cc: when replying." + :type 'boolean + :group 'mu4e-compose) + +(defvar mu4e-compose-parent-message nil + "The parent message plist. +This is the message being replied to, forwarded or edited; used +in `mu4e-compose-pre-hook'. For new messages, it is nil.") + +;;;; Calendar + +(defgroup mu4e-icalendar nil + "Icalendar related settings." + :group 'mu4e) + +(defcustom mu4e-icalendar-trash-after-reply nil + "If non-nil, trash the icalendar invitation after replying." + :type 'boolean + :group 'mu4e-icalendar) + +(defcustom mu4e-icalendar-diary-file nil + "If non-nil, the file in which to add events upon reply." + :type '(choice (const :tag "Do not insert a diary entry" nil) + (string :tag "Insert a diary entry in this file")) + :group 'mu4e-icalendar) + + +;;;; Folders + +(defgroup mu4e-folders nil + "Special folders." + :group 'mu4e) + +(defcustom mu4e-drafts-folder "/drafts" + "Your folder for draft messages, relative to the root maildir. +For instance, \"/drafts\". Instead of a string, may also be a +function that takes a message (a msg plist, see +`mu4e-message-field'), and returns a folder. Note, the message +parameter refers to the original message being replied to / being +forwarded / re-edited and is nil otherwise. `mu4e-drafts-folder' +is only evaluated once." + :type '(choice + (string :tag "Folder name") + (function :tag "Function return folder name")) + :group 'mu4e-folders) + +(defcustom mu4e-refile-folder "/archive" + "Your folder for refiling messages, relative to the root maildir. +For instance \"/Archive\". Instead of a string, may also be a +function that takes a message (a msg plist, see +`mu4e-message-field'), and returns a folder. Note that the +message parameter refers to the message-at-point." + :type '(choice + (string :tag "Folder name") + (function :tag "Function return folder name")) + :group 'mu4e-folders) + +(defcustom mu4e-sent-folder "/sent" + "Your folder for sent messages, relative to the root maildir. +For instance, \"/Sent Items\". Instead of a string, may also be a +function that takes a message (a msg plist, see +`mu4e-message-field'), and returns a folder. Note that the +message parameter refers to the original message being replied to +/ being forwarded / re-edited, and is nil otherwise." + :type '(choice + (string :tag "Folder name") + (function :tag "Function return folder name")) + :group 'mu4e-folders) + +(defcustom mu4e-trash-folder "/trash" + "Your folder for trashed messages, relative to the root maildir. +For instance, \"/trash\". Instead of a string, may also be a +function that takes a message (a msg plist, see +`mu4e-message-field'), and returns a folder. When using +`mu4e-trash-folder' in the headers view (when marking messages +for trash). Note that the message parameter refers to the +message-at-point. When using it when composing a message (see +`mu4e-sent-messages-behavior'), this refers to the original +message being replied to / being forwarded / re-edited, and is +nil otherwise." + :type '(choice + (string :tag "Folder name") + (function :tag "Function return folder name")) + :group 'mu4e-folders) + +(defcustom mu4e-maildir-shortcuts nil + "A list of maildir shortcuts. +This makes it possible to quickly go to a particular +maildir (folder), or quickly moving messages to them (e.g., for +archiving or refiling). + +Each of the list elements is a plist with at least: +`:maildir' - the maildir for the shortcut (e.g. \"/archive\") +`:key' - the shortcut key. + +Optionally, you can add the following: +`:hide' - if t, the shortcut is hidden from the main-view and +speedbar. +`:hide-unread' - do not show the counts of unread/total number + of matches for the maildir in the main-view, and is implied +from `:hide'. + +For backward compatibility, an older form is recognized as well: + + (maildir . key), where MAILDIR is a maildir (such as +\"/archive/\"), and key is a single character. + +You can use these shortcuts in the headers and view buffers, for +example with `mu4e-mark-for-move-quick' (or 'm', by default) or +`mu4e-jump-to-maildir' (or 'j', by default), followed by the +designated shortcut character for the maildir. + +Unlike in search queries, folder names with spaces in them must +NOT be quoted, since mu4e does this for you." + :type '(repeat (cons (string :tag "Maildir") character)) + :version "1.3.9" + :group 'mu4e-folders) + +(defcustom mu4e-maildir-info-delimiter + (if (member system-type '(ms-dos windows-nt cygwin)) + ";" ":") + "Separator character between message identifier and flags. +It defaults to ':' on most platforms, except on Windows, +where it is not allowed and we use ';' for compatibility +with mbsync, offlineimap and other programs." + :type 'string + :group 'mu4e-folders) + + +(defun mu4e-maildir-shortcuts () + "Get `mu4e-maildir-shortcuts' in the (new) format, converting +from the old format if needed." + (cl-map 'list + (lambda (item) ;; convert from old format? + (if (and (consp item) (not (consp (cdr item)))) + `(:maildir ,(car item) :key ,(cdr item)) + item)) + mu4e-maildir-shortcuts)) + +(defcustom mu4e-display-update-status-in-modeline nil + "Non-nil value will display the update status in the modeline." + :group 'mu4e + :type 'boolean) + ;;; Faces (defgroup mu4e-faces nil @@ -150,6 +773,11 @@ I.e. a message with the draft flag set." "Face for a header title in the headers view." :group 'mu4e-faces) +(defface mu4e-context-face + '((t :inherit mu4e-title-face :weight bold)) + "Face for displaying the context in the modeline." + :group 'mu4e-faces) + (defface mu4e-modeline-face '((t :inherit font-lock-string-face :weight bold)) "Face for the query in the mode-line." @@ -420,8 +1048,146 @@ header-view, not including, for instance, the message body.") ;;;; Main +(defvar mu4e-main-buffer-name " *mu4e-main*" + "Name of the mu4e main view buffer. The default name starts +with SPC and therefore is not visible in buffer list.") + + +;;;; Headers + +(defconst mu4e~headers-buffer-name "*mu4e-headers*" + "Name of the buffer for message headers.") + +(defvar mu4e~headers-last-query nil + "The present (most recent) query.") + +;;;; View + +(defconst mu4e~view-buffer-name "*mu4e-view*" + "Name for the message view buffer.") + +(defconst mu4e~view-embedded-buffer-name " *mu4e-embedded-view*" + "Name for the embedded message view buffer.") + ;;;; Other +(defvar mu4e~contacts-hash nil + "Hash that maps contacts (ie. 'name ') to an integer for sorting. +We need to keep this information around to quickly re-sort +subsets of the contacts in the completions function in +mu4e-compose.") + +(defvar mu4e~server-props nil + "Information we receive from the mu4e server process \(in the 'pong-handler').") + +(defun mu4e-root-maildir() + "Get the root maildir." + (let ((root-maildir (and mu4e~server-props + (plist-get mu4e~server-props :root-maildir)))) + (unless root-maildir + (mu4e-error "root maildir unknown; did you start mu4e?")) + root-maildir)) + +(defun mu4e-database-path() + "Get the mu4e database path" + (let ((path (and mu4e~server-props + (plist-get mu4e~server-props :database-path)))) + (unless path + (mu4e-error "database-path unknown; did you start mu4e?")) + path)) + +(defun mu4e-personal-addresses(&optional no-regexp) + "Get the list user's personal addresses, as passed to `mu init --my-address=...'. + The address are either plain e-mail address or /regular + expressions/. When NO_REGEXP is non-nil, do not include regexp + address patterns (if any)." + (seq-remove + (lambda(addr) (and no-regexp (string-match-p "^/.*/" addr))) + (when mu4e~server-props (plist-get mu4e~server-props :personal-addresses)))) + +(defun mu4e-server-version() + "Get the server version, which should match mu4e's." + (let ((version (and mu4e~server-props (plist-get mu4e~server-props :version)))) + (unless version + (mu4e-error "version unknown; did you start mu4e?")) + version)) + + +;;; Handler functions +;; +;; The handler functions define what happens when we receive a certain +;; message from the server. Here we register our handler functions; +;; these connect server messages to functions to handle them. +;; +;; These bindings form mu4e's central nervous system so it's not +;; really recommended to override them (they reference various +;; internal bits, which could change). + +(defun mu4e~default-handler (&rest args) + "Dummy handler function with arbitrary ARGS." + (error "Not handled: %S" args)) + +(defvar mu4e-error-func 'mu4e-error-handler + "Function called for each error received. +The function is passed an error plist as argument. See +`mu4e~proc-filter' for the format.") + +(defvar mu4e-update-func 'mu4e~headers-update-handler + "Function called for each :update sexp returned. +The function is passed a msg sexp as argument. +See `mu4e~proc-filter' for the format.") + +(defvar mu4e-remove-func 'mu4e~headers-remove-handler + "Function called for each :remove sexp returned. +This happens when some message has been deleted. The function is +passed the docid of the removed message.") + +(defvar mu4e-sent-func 'mu4e~default-handler + "Function called for each :sent sexp received. +This happens when some message has been sent. The function is +passed the docid and the draft-path of the sent message.") + +(defvar mu4e-view-func 'mu4e~headers-view-handler + "Function called for each single-message sexp. +The function is passed a message sexp as argument. See +`mu4e~proc-filter' for the format.") + +(defvar mu4e-header-func 'mu4e~headers-header-handler + "Function called for each message-header received. +The function is passed a msg plist as argument. See +`mu4e~proc-filter' for the format.") + +(defvar mu4e-found-func 'mu4e~headers-found-handler + "Function called for when we received a :found sexp. +This happens after the headers have been returned, to report on +the number of matches. See `mu4e~proc-filter' for the format.") + +(defvar mu4e-erase-func 'mu4e~headers-clear + "Function called we receive an :erase sexp. +This before new headers are displayed, to clear the current +headers buffer. See `mu4e~proc-filter' for the format.") + +(defvar mu4e-compose-func 'mu4e~compose-handler + "Function called for each compose message received. +I.e., the original message that is used as basis for composing a +new message (i.e., either a reply or a forward); the function is +passed msg and a symbol (either reply or forward). See +`mu4e~proc-filter' for the format of .") + +(defvar mu4e-info-func 'mu4e-info-handler + "Function called for each (:info type ....) sexp received. +from the server process.") + +(defvar mu4e-pong-func 'mu4e~default-handler + "Function called for each (:pong type ....) sexp received.") + +(defvar mu4e-contacts-func 'mu4e-contacts-func + "A function called for each (:contacts () +sexp received from the server process.") + +(defvar mu4e-temp-func 'mu4e~view-temp-handler + "A function called for each (:temp ) sexp.") + ;;; Internals (defvar mu4e~headers-view-win nil diff --git a/mu4e/mu4e-view-common.el b/mu4e/mu4e-view-common.el new file mode 100644 index 00000000..e4880d53 --- /dev/null +++ b/mu4e/mu4e-view-common.el @@ -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 +;; Maintainer: 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 . + +;;; 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 in browser" . mu4e-action-view-in-browser) + ("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 "") '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-") '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) diff --git a/mu4e/mu4e-view-gnus.el b/mu4e/mu4e-view-gnus.el new file mode 100644 index 00000000..f4350e2e --- /dev/null +++ b/mu4e/mu4e-view-gnus.el @@ -0,0 +1,657 @@ +;;; 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 +;; Maintainer: 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 . + +;;; 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 MSG as a string." + ;; we need this for replying/forwarding, since the mu4e-compose + ;; wants it that way. + (with-temp-buffer + (insert-file-contents-literally + (mu4e-message-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 MSG in browser if it includes an HTML-part. +The variables `browse-url-browser-function', +`browse-url-handlers', and `browse-url-default-handlers' +determine which browser function to use." + (with-temp-buffer + (insert-file-contents-literally + (mu4e-message-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." + (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) + "Like `gnus-display-mime' but include mu4e headers to MSG." + (lambda (&optional ihandles) + (gnus-display-mime ihandles) + (unless ihandles + (save-restriction + (article-goto-body) + (forward-line -1) + (narrow-to-region (point) (point)) + (dolist (field mu4e-view-fields) + (let ((fieldval (mu4e-message-field msg field))) + (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." + (let* ((info (cdr (assoc field mu4e-header-info))) + (key (plist-get info :name)) + (help (plist-get info :help))) + (if (and val (> (length val) 0)) + (insert (propertize (concat key ":") 'help-echo help) + " " val "\n")))) + +(defun mu4e~view-gnus-insert-header-custom (msg field) + "Insert MSG's custom FIELD." + (let* ((info (cdr-safe (or (assoc field mu4e-header-info-custom) + (mu4e-error "Custom field %S not found" field)))) + (key (plist-get info :name)) + (func (or (plist-get info :function) + (mu4e-error "No :function defined for custom field %S %S" + field info))) + (val (funcall func msg)) + (help (plist-get info :help))) + (when (and val (> (length val) 0)) + (insert (propertize (concat key ":") 'help-echo help) " " val "\n")))) + +(define-advice gnus-icalendar-event-from-handle + (:filter-args (handle-attendee) mu4e~view-fix-missing-charset) + "Avoid error when displaying an ical attachment without a charset." + (if (and (boundp 'mu4e~view-rendering) mu4e~view-rendering) + (let* ((handle (car handle-attendee)) + (attendee (cadr handle-attendee)) + (buf (mm-handle-buffer handle)) + (ty (mm-handle-type handle)) + (rest (cddr handle))) + ;; Put the fallback at the end: + (setq ty (append ty '((charset . "utf-8")))) + (setq handle (cons buf (cons ty rest))) + (list handle attendee)) + handle-attendee)) + +(defun mu4e~view-mode-p () + "Is the buffer in mu4e-view-mode or one of its descendants?" + (or (eq major-mode 'mu4e-view-mode) + (derived-mode-p '(mu4e-view-mode)))) + +(defun mu4e~view-nop (func &rest args) + "Do not invoke FUNC with ARGS when in mu4e-view-mode. +This is useful for advising some Gnus-functionality that does not work in mu4e." + (unless (mu4e~view-mode-p) + (apply func args))) + +(defun mu4e~view-button-reply (func &rest args) + "Advise FUNC with ARGS to make `gnus-button-reply' links work in mu4e." + (if (mu4e~view-mode-p) + (mu4e-compose-reply) + (apply func args))) + +(defun mu4e~view-msg-mail (func &rest args) + "Advise FUNC with ARGS to make `gnus-msg-mail' links compose with mu4e." + (if (mu4e~view-mode-p) + (apply 'mu4e~compose-mail args) + (apply func args))) + +(defvar mu4e-view-mode-map + (let ((map (make-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 "") #'mu4e-headers-query-prev) + (define-key map (kbd "") #'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 "") #'mu4e-headers-split-view-grow) + (define-key map (kbd "") #'mu4e-headers-split-view-shrink) + + ;; intra-message navigation + (define-key map (kbd "S-SPC") #'scroll-down) + (define-key map (kbd "SPC") #'mu4e-view-scroll-up-or-next) + (define-key map (kbd "RET") #'mu4e-scroll-up) + (define-key map (kbd "") #'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 "") #'mu4e-view-headers-next) + (define-key map (kbd "") #'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 "") #'mu4e-view-mark-for-delete) + (define-key map (kbd "") #'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 "") #'mu4e-view-mark-for-something) + (define-key map (kbd "") #'mu4e-view-mark-for-something) + (define-key map (kbd "") #'mu4e-view-mark-for-something) + + (define-key map (kbd "#") #'mu4e-mark-resolve-deferred-marks) + + ;; misc + (define-key map "M" #'mu4e-view-massage) + + (define-key map "w" 'visual-line-mode) + (define-key map "h" #'mu4e-view-toggle-html) + (define-key map (kbd "M-q") 'article-fill-long-lines) + + ;; 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) + + ;; advice gnus-block-private-groups to always return "." + ;; so that by default we block images. + (advice-add 'gnus-block-private-groups :around + (lambda(func &rest args) + (if (mu4e~view-mode-p) + "." (apply func args)))) + (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. +The alist uniquely maps the number to the gnus-part." + (let ((parts '())) + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (let ((part (get-text-property (point) 'gnus-data)) + (index (get-text-property (point) 'gnus-part))) + (when (and part (numberp index) (not (assoc index parts)) + (push `(,index . ,part) parts))) + (goto-char (or (next-single-property-change (point) 'gnus-part) + (point-max)))))) + parts)) + + +(defun mu4e-view-save-attachments (&optional arg) + "Save mime parts from current mu4e gnus view buffer. + +When helm-mode is enabled provide completion on attachments and +possibility to mark candidates to save, otherwise completion on +attachments is done with `completing-read-multiple', in this case +use \",\" to separate candidate, completion is provided after +each \",\". + +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 (let ((file (expand-file-name f dir))) + (if (file-exists-p file) + (let (newname (count 1)) + (while (and + (setq newname + (concat + (file-name-sans-extension file) + (format "(%s)" count) + (file-name-extension file t))) + (file-exists-p newname)) + (cl-incf count)) + newname) + file))))) + (mu4e-message "No attached files found")))) + + +(defvar mu4e-view-mime-part-actions + '( + ;; + ;; some basic ones + ;; + + ;; save MIME-part to a file + (:name "save" :handler gnus-article-save-part :receives index) + ;; pipe MIME-part to some arbitrary shell command + (:name "|pipe" :handler gnus-article-pipe-part :receives index) + ;; open with the default handler, if any + (:name "open" :handler mu4e~view-open-file :receives temp) + ;; open with some custom file. + (:name "wopen-with" :handler (lambda (file)(mu4e~view-open-file file t)) + :receives temp) + + ;; + ;; some more examples + ;; + + ;; import GPG key + (:name "gpg" :handler epa-import-keys :receives temp) + ;; count the number of lines in a MIME-part + (:name "line-count" :handler "wc -l" :receives pipe) + ;; open in this emacs instance; tries to use the attachment name, + ;; so emacs can use specific modes etc. + (:name "emacs" :handler find-file :receives temp) + ;; open in this emacs instance, "raw" + (:name "raw" :handler (lambda (str) + (let ((tmpbuf (get-buffer-create " *mu4e-raw-mime*"))) + (with-current-buffer tmpbuf + (insert str) + (view-mode) + (goto-char (point-min))) + (switch-to-buffer tmpbuf))) :receives pipe)) + + "Specifies actions for MIME-parts. + +Each of the actions is a plist with keys +`(:name ;; name of the action; shortcut is first letter of name + + :handler ;; one of: + ;; - a function receiving the index/temp/pipe + ;; - a string, which is taken as a shell command + + :receives ;; a symbol specifying what the handler receives + ;; - index: the index number of the mime part (default) + ;; - temp: the full path to the mime part in a + ;; temporary file, which is deleted immediately + ;; after invoking handler + ;; - pipe: the attachment is piped to some shell command + ;; or as a string parameter to a function +).") + + +(defun mu4e~view-mime-part-to-temp-file (handle) + "Write MIME-part HANDLE to a temporary file and return the file name. +The filename is deduced from the MIME-part's filename, or +otherwise random; the result is placed in a temporary directory +with a unique name. Returns the full path for the file created. +The directory and file are self-destructed." + (let* ((tmpdir (make-temp-file "mu4e-temp-" t)) + (fname (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 to MIME-part N in the current messsage. +If N is not specified, ask for it. For instance, '3 A o' opens +the third MIME-part." + (interactive "NNumber of MIME-part: ") + (let* ((parts (mu4e~view-gather-mime-parts)) + (options (mapcar (lambda (action) `(,(plist-get action :name) . ,action)) + mu4e-view-mime-part-actions)) + (handle (or (cdr-safe (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 item found for action %S" action))) + (receives (or (plist-get action :receives) + (mu4e-error "No :receives item found for action %S" action)))) + (save-excursion + (cond + ((functionp handler) + (cond + ((eq receives 'index) (funcall handler n)) + ((eq receives 'pipe) (funcall handler (mm-with-unibyte-buffer + (mm-insert-part handle) + (buffer-string)))) + ((eq receives 'temp) + (funcall handler (mu4e~view-mime-part-to-temp-file handle))) + (t (mu4e-error "Invalid :receive for %S" action)))) + ((stringp handler) + (cond + ((eq receives 'index) (shell-command (concat handler " " (shell-quote-argument n)))) + ((eq receives 'pipe) (mm-pipe-part handle handler)) + ((eq receives 'temp) + (shell-command (shell-command (concat handler " " + (shell-quote-argument + (mu4e~view-mime-part-to-temp-file handle)))))) + (t (mu4e-error "Invalid action %S" action)))))))) + +(defun mu4e-view-toggle-html () + "Toggle html-display of the first html-part found." + (interactive) + ;; This function assumes `gnus-article-mime-handle-alist' is sorted by + ;; pertinence, i.e. the first HTML part found in it is the most important one. + (if-let ((html-part + (seq-find (lambda (handle) + (equal (mm-handle-media-type (cdr handle)) "text/html")) + gnus-article-mime-handle-alist))) + (gnus-article-inline-part (car html-part)) + (mu4e-warn "No html part in this message"))) + + +(provide 'mu4e-view-gnus) +;;; mu4e-view-gnus.el ends here diff --git a/mu4e/mu4e-view-old.el b/mu4e/mu4e-view-old.el new file mode 100644 index 00000000..edd7981d --- /dev/null +++ b/mu4e/mu4e-view-old.el @@ -0,0 +1,1097 @@ +;;; mu4e-view-old.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 +;; Maintainer: 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 . + +;;; Commentary: + +;; In this file we define mu4e-view-mode (+ helper functions), which is used for +;; viewing e-mail messages + +;;; Code: + +(require 'cl-lib) +(require 'mu4e-view-common) + +(declare-function mu4e-view "mu4e-view") + +;;; Internal variables + +(defvar mu4e-view-fill-headers t + "If non-nil, automatically fill the headers when viewing them.") + +(defvar mu4e~view-cited-hidden nil "Whether cited lines are hidden.") +(put 'mu4e~view-cited-hidden 'permanent-local t) + +(defvar mu4e~path-parent-docid-map (make-hash-table :test 'equal) + "A map of msg paths --> parent-docids. +This is to determine what is the parent docid for embedded +message extracted at some path.") +(put 'mu4e~path-parent-docid-map 'permanent-local t) + +(defvar mu4e~view-attach-map nil + "A mapping of user-visible attachment number to the actual part index.") +(put 'mu4e~view-attach-map 'permanent-local t) + +(defvar mu4e~view-rendering nil) + +(defvar mu4e~view-html-text nil + "Should we prefer html or text just this once? A symbol `text' +or `html' or nil.") + +;;; Main + +(defun mu4e~view-custom-field (msg field) + "Show some custom header field, or raise an error if it is not +found." + (let* ((item (or (assoc field mu4e-header-info-custom) + (mu4e-error "field %S not found" field))) + (func (or (plist-get (cdr-safe item) :function) + (mu4e-error "no :function defined for field %S %S" + field (cdr item))))) + (funcall func msg))) + +(defun mu4e-view-message-text (msg) + "Return the message to display (as a string), based on the MSG plist." + (concat + (mapconcat + (lambda (field) + (let ((fieldval (mu4e-message-field msg field))) + (cl-case field + (:subject (mu4e~view-construct-header field fieldval)) + (:path (mu4e~view-construct-header field fieldval)) + (:maildir (mu4e~view-construct-header field fieldval)) + (:user-agent (mu4e~view-construct-header field fieldval)) + ((:flags :tags) (mu4e~view-construct-flags-tags-header + field fieldval)) + + ;; contact fields + (:to (mu4e~view-construct-contacts-header msg field)) + (:from (mu4e~view-construct-contacts-header msg field)) + (:cc (mu4e~view-construct-contacts-header msg field)) + (:bcc (mu4e~view-construct-contacts-header msg field)) + + ;; if we (`user-mail-address' are the From, show To, otherwise, + ;; show From + (:from-or-to + (let* ((from (mu4e-message-field msg :from)) + (from (and from (cdar from)))) + (if (mu4e-personal-address-p from) + (mu4e~view-construct-contacts-header msg :to) + (mu4e~view-construct-contacts-header msg :from)))) + ;; date + (:date + (let ((datestr + (when fieldval (format-time-string mu4e-view-date-format + fieldval)))) + (if datestr (mu4e~view-construct-header field datestr) ""))) + ;; size + (:size + (mu4e~view-construct-header field (mu4e-display-size fieldval))) + (:mailing-list + (mu4e~view-construct-header field fieldval)) + (:message-id + (mu4e~view-construct-header field fieldval)) + ;; attachments + (:attachments (mu4e~view-construct-attachments-header msg)) + ;; pgp-signatures + (:signature (mu4e~view-construct-signature-header msg)) + ;; pgp-decryption + (:decryption (mu4e~view-construct-decryption-header msg)) + (t (mu4e~view-construct-header field + (mu4e~view-custom-field msg field)))))) + mu4e-view-fields "") + "\n" + (let* ((prefer-html + (cond + ((eq mu4e~view-html-text 'html) t) + ((eq mu4e~view-html-text 'text) nil) + (t mu4e-view-prefer-html))) + (body (mu4e-message-body-text msg prefer-html))) + (setq mu4e~view-html-text nil) + (when (fboundp 'add-face-text-property) + (add-face-text-property 0 (length body) 'mu4e-view-body-face t body)) + body))) + +(defun mu4e~view-embedded-winbuf () + "Get a buffer (shown in a window) for the embedded message." + (let* ((buf (get-buffer-create mu4e~view-embedded-buffer-name)) + (win (or (get-buffer-window buf) (split-window-vertically)))) + (select-window win) + (switch-to-buffer buf))) + +(defun mu4e~delete-all-overlays () + "`delete-all-overlays' with compatibility fallback." + (if (functionp 'delete-all-overlays) + (delete-all-overlays) + (remove-overlays))) + +(defun mu4e~view-old (msg) + "Display MSG using mu4e's internal view mode." + (let* ((embedded ;; is it as an embedded msg (ie. message/rfc822 att)? + (when (gethash (mu4e-message-field msg :path) + mu4e~path-parent-docid-map) t)) + (buf (if embedded + (mu4e~view-embedded-winbuf) + (get-buffer-create mu4e~view-buffer-name)))) + + ;; XXX(djcb): only called for the side-effect of setting up + ;; `mu4e~view-attach-map'. Instead, we should split that function + ;; into setting up the map, and actually producing the header. + (mu4e~view-construct-attachments-header msg) + + (with-current-buffer buf + (let ((inhibit-read-only t)) + (erase-buffer) + (mu4e~delete-all-overlays) + (insert (mu4e-view-message-text msg)) + (goto-char (point-min)) + (mu4e~fontify-cited) + (mu4e~fontify-signature) + (mu4e~view-activate-urls) + (mu4e~view-show-images-maybe msg) + (when (not embedded) (setq mu4e~view-message msg)) + (mu4e-view-mode) + (when embedded (local-set-key "q" 'kill-buffer-and-window))) + (switch-to-buffer buf)))) + + +(defun mu4e~view-construct-header (field val &optional dont-propertize-val) + "Return header field FIELD (as in `mu4e-header-info') with value +VAL if VAL is non-nil. If DONT-PROPERTIZE-VAL is non-nil, do not +add text-properties to VAL." + (let* ((info (cdr (assoc field + (append mu4e-header-info mu4e-header-info-custom)))) + (key (plist-get info :name)) + (val (if val (propertize val 'field 'mu4e-header-field-value + 'front-sticky '(field)))) + (help (plist-get info :help))) + (if (and val (> (length val) 0)) + (with-temp-buffer + (insert (propertize (concat key ":") + 'field 'mu4e-header-field-key + 'front-sticky '(field) + 'keymap mu4e-view-header-field-keymap + 'face 'mu4e-header-key-face + 'help-echo help) " " + (if dont-propertize-val + val + (propertize val 'face 'mu4e-header-value-face)) "\n") + (when mu4e-view-fill-headers + ;; temporarily set the fill column positions to the right, so + ;; we can indent the following lines correctly + (let* ((margin 1) + (fill-column (max (- fill-column margin) 0))) + (fill-region (point-min) (point-max)) + (goto-char (point-min)) + (while (and (zerop (forward-line 1)) (not (looking-at "^$"))) + (indent-to-column margin)))) + (buffer-string)) + ""))) + +(defun mu4e~view-header-field-fold () + "Fold/unfold headers' value if there is more than one line." + (interactive) + (let ((name-pos (field-beginning)) + (value-pos (1+ (field-end)))) + (if (and name-pos value-pos + (eq (get-text-property name-pos 'field) 'mu4e-header-field-key)) + (save-excursion + (let* ((folded)) + (mapc (lambda (o) + (when (overlay-get o 'mu4e~view-header-field-folded) + (delete-overlay o) + (setq folded t))) + (overlays-at value-pos)) + (unless folded + (let* ((o (make-overlay value-pos (field-end value-pos))) + (vals (split-string (field-string value-pos) "\n" t)) + (val (if (= (length vals) 1) + (car vals) + (truncate-string-to-width (car vals) + (- (length (car vals)) 1) 0 nil t)))) + (overlay-put o 'mu4e~view-header-field-folded t) + (overlay-put o 'display val)))))))) + +(defun mu4e~view-compose-contact (&optional point) + "Compose a message for the address at point." + (interactive) + (unless (get-text-property (or point (point)) 'email) + (mu4e-error "No address at point")) + (mu4e~compose-mail (get-text-property (or point (point)) 'long))) + +(defun mu4e~view-copy-contact (&optional full) + "Compose a message for the address at (point)." + (interactive "P") + (let ((email (get-text-property (point) 'email)) + (long (get-text-property (point) 'long))) + (unless email (mu4e-error "No address at point")) + (kill-new (if full long email)) + (mu4e-message "Address copied."))) + +(defun mu4e~view-construct-contacts-header (msg field) + "Add a header for a contact field (ie., :to, :from, :cc, :bcc)." + (mu4e~view-construct-header field + (mapconcat + (lambda(c) + (let* ((name (when (car c) + (replace-regexp-in-string "[[:cntrl:]]" "" (car c)))) + (email (when (cdr c) + (replace-regexp-in-string "[[:cntrl:]]" "" (cdr c)))) + (short (or name email)) ;; name may be nil + (long (if name (format "%s <%s>" name email) email))) + (propertize + (if mu4e-view-show-addresses long short) + 'long long + 'short short + 'email email + 'keymap mu4e-view-contacts-header-keymap + 'face 'mu4e-contact-face + 'mouse-face 'highlight + 'help-echo (format "<%s>\n%s" email + "[mouse-2] or C to compose a mail for this recipient")))) + (mu4e-message-field msg field) ", ") t)) + +(defun mu4e~view-construct-flags-tags-header (field val) + "Construct a Flags: header." + (mu4e~view-construct-header + field + (mapconcat + (lambda (flag) + (propertize + (if (symbolp flag) + (symbol-name flag) + flag) + 'face 'mu4e-special-header-value-face)) + val + (propertize ", " 'face 'mu4e-header-value-face)) t)) + +(defun mu4e~view-construct-signature-header (msg) + "Construct a Signature: header, if there are any signed parts." + (let* ((parts (mu4e-message-field msg :parts)) + (verdicts + (cl-remove-if 'null + (mapcar (lambda (part) (mu4e-message-part-field part :signature)) + parts))) + (signers + (mapconcat 'identity + (cl-remove-if 'null + (mapcar (lambda (part) (mu4e-message-part-field part :signers)) + parts)) ", ")) + (val (when verdicts + (mapconcat + (lambda (v) + (propertize (symbol-name v) + 'face (if (eq v 'verified) + 'mu4e-ok-face 'mu4e-warning-face))) + verdicts ", "))) + (btn (when val + (with-temp-buffer + (insert-text-button "Details" + 'action (lambda (b) + (mu4e-view-verify-msg-popup + (button-get b 'msg)))) + (buffer-string)))) + (val (when val (concat val " " signers " (" btn ")")))) + (mu4e~view-construct-header :signature val t))) + +(defun mu4e~view-construct-decryption-header (msg) + "Construct a Decryption: header, if there are any encrypted parts." + (let* ((parts (mu4e-message-field msg :parts)) + (verdicts + (cl-remove-if 'null + (mapcar (lambda (part) + (mu4e-message-part-field part :decryption)) + parts))) + (succeeded (cl-remove-if (lambda (v) (eq v 'failed)) verdicts)) + (failed (cl-remove-if (lambda (v) (eq v 'succeeded)) verdicts)) + (succ (when succeeded + (propertize + (concat (number-to-string (length succeeded)) + " part(s) decrypted") + 'face 'mu4e-ok-face))) + (fail (when failed + (propertize + (concat (number-to-string (length failed)) + " part(s) failed") + 'face 'mu4e-warning-face))) + (val (concat succ fail))) + (mu4e~view-construct-header :decryption val t))) + +(defun mu4e~view-open-attach-from-binding () + "Open the attachment at point, or click location." + (interactive) + (let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg)) + ( attnum (mu4e~view-get-property-from-event 'mu4e-attnum))) + (when (and msg attnum) + (mu4e-view-open-attachment msg attnum)))) + +(defun mu4e~view-save-attach-from-binding () + "Save the attachment at point, or click location." + (interactive) + (let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg)) + ( attnum (mu4e~view-get-property-from-event 'mu4e-attnum))) + (when (and msg attnum) + (mu4e-view-save-attachment-single msg attnum)))) + +(defun mu4e~view-construct-attachments-header (msg) + "Display attachment information; the field looks like something like: + :parts ((:index 1 :name \"1.part\" :mime-type \"text/plain\" + :type (leaf) :attachment nil :size 228) + (:index 2 :name \"analysis.doc\" + :mime-type \"application/msword\" + :type (leaf attachment) :attachment nil :size 605196))" + (setq mu4e~view-attach-map ;; buffer local + (make-hash-table :size 64 :weakness nil)) + (let* ((id 0) + (partcount (length (mu4e-message-field msg :parts))) + (attachments + ;; we only list parts that look like attachments, ie. that have a + ;; non-nil :attachment property; we record a mapping between + ;; user-visible numbers and the part indices + (cl-remove-if-not + (lambda (part) + (let* ((mtype (or (mu4e-message-part-field part :mime-type) + "application/octet-stream")) + (partsize (or (mu4e-message-part-field part :size) 0)) + (attachtype (mu4e-message-part-field part :type)) + (isattach + (or ;; we consider parts marked either + ;; "attachment" or "inline" as attachment. + (member 'attachment attachtype) + ;; list inline parts as attachment (so they can be + ;; saved), unless they are text/plain, which are + ;; usually just message footers in mailing lists + ;; + ;; however, slow bigger text parts as attachments, + ;; except when they're the only part... it's + ;; complicated. + (and (member 'inline attachtype) + (or + (and (> partcount 1) (> partsize 256)) + (not (string-match "^text/plain" mtype))))))) + (or ;; remove if it's not an attach *or* if it's an + ;; image/audio/application type (but not a signature) + isattach + (string-match "^\\(image\\|audio\\)" mtype) + (string= "message/rfc822" mtype) + (string= "text/calendar" mtype) + (and (string-match "^application" mtype) + (not (string-match "signature" mtype)))))) + (mu4e-message-field msg :parts))) + (attstr + (mapconcat + (lambda (part) + (let ((index (mu4e-message-part-field part :index)) + (name (mu4e-message-part-field part :name)) + (size (mu4e-message-part-field part :size))) + (cl-incf id) + (puthash id index mu4e~view-attach-map) + + (concat + (propertize (format "[%d]" id) + 'face 'mu4e-attach-number-face) + (propertize name 'face 'mu4e-link-face + 'keymap mu4e-view-attachments-header-keymap + 'mouse-face 'highlight + 'help-echo (concat + "[mouse-1] or [M-RET] opens the attachment\n" + "[mouse-2] or [S-RET] offers to save it") + 'mu4e-msg msg + 'mu4e-attnum id + ) + (when (and size (> size 0)) + (propertize (format "(%s)" (mu4e-display-size size)) + 'face 'mu4e-header-key-face))))) + attachments ", "))) + (when attachments + (mu4e~view-construct-header :attachments attstr t)))) + +(defun mu4e-view-for-each-part (msg func) + "Apply FUNC to each part in MSG. +FUNC should be a function taking two arguments: + 1. the message MSG, and + 2. a plist describing the attachment. The plist looks like: + (:index 1 :name \"test123.doc\" + :mime-type \"application/msword\" :attachment t :size 1234)." + (dolist (part (mu4e-msg-field msg :parts)) + (funcall func msg part))) + +(defvar mu4e-view-mode-map nil + "Keymap for \"*mu4e-view*\" buffers.") +(unless mu4e-view-mode-map + (setq 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 "") 'mu4e-headers-query-prev) + (define-key map (kbd "") '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 "v" 'mu4e-view-verify-msg-popup) + + (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 ";" '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 "") 'mu4e-headers-split-view-grow) + (define-key map (kbd "") '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 "") '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 "") 'mu4e-view-headers-next) + (define-key map (kbd "") 'mu4e-view-headers-prev) + + (define-key map (kbd "[") 'mu4e-view-headers-prev-unread) + (define-key map (kbd "]") 'mu4e-view-headers-next-unread) + + ;; switching to view mode (if it's visible) + (define-key map "y" 'mu4e-select-other-view) + + ;; attachments + (define-key map "e" 'mu4e-view-save-attachment) + (define-key map "o" 'mu4e-view-open-attachment) + (define-key map "A" 'mu4e-view-attachment-action) + + ;; marking/unmarking + (define-key map "d" 'mu4e-view-mark-for-trash) + (define-key map (kbd "") 'mu4e-view-mark-for-delete) + (define-key map (kbd "") '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 "") 'mu4e-view-mark-for-something) + (define-key map (kbd "") 'mu4e-view-mark-for-something) + (define-key map (kbd "") 'mu4e-view-mark-for-something) + + (define-key map (kbd "#") 'mu4e-mark-resolve-deferred-marks) + + ;; misc + (define-key map "w" 'visual-line-mode) + (define-key map "#" 'mu4e-view-toggle-hide-cited) + (define-key map "h" 'mu4e-view-toggle-html) + (define-key map (kbd "M-q") 'mu4e-view-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 [toggle-html] + '("Toggle view-html" . mu4e-view-toggle-html)) + (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 [sepa8] '("--")) + (define-key menumap [open-att] + '("Open attachment" . mu4e-view-open-attachment)) + (define-key menumap [extract-att] + '("Extract attachment" . mu4e-view-save-attachment)) + (define-key menumap [save-url] + '("Save URL to kill-ring" . mu4e-view-save-url)) + (define-key menumap [fetch-url] + '("Fetch URL" . mu4e-view-fetch-url)) + (define-key menumap [goto-url] + '("Visit URL" . mu4e-view-go-to-url)) + + (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))) + map)) + + (fset 'mu4e-view-mode-map 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-derived-mode mu4e-view-mode special-mode "mu4e:oldview" + "Major mode for viewing an e-mail message in mu4e." + (mu4e~view-mode-body)) + +(defun mu4e~view-show-images-maybe (msg) + "Show attached images, if `mu4e-show-images' is non-nil." + (when (and (display-images-p) mu4e-view-show-images) + (mu4e-view-for-each-part msg + (lambda (_msg part) + (when (string-match "^image/" + (or (mu4e-message-part-field part :mime-type) + "application/object-stream")) + (let ((imgfile (mu4e-message-part-field part :temp))) + (when (and imgfile (file-exists-p imgfile)) + (save-excursion + (goto-char (point-max)) + (mu4e-display-image imgfile + mu4e-view-image-max-width + mu4e-view-image-max-height))))))))) + + +(defun mu4e~view-hide-cited () + "Toggle hiding of cited lines in the message body." + (save-excursion + (let ((inhibit-read-only t)) + (goto-char (point-min)) + (flush-lines mu4e-cited-regexp) + (setq mu4e~view-cited-hidden t)))) + + +;;; Interactive functions + +(defun mu4e-view-toggle-hide-cited () + "Toggle hiding of cited lines in the message body." + (interactive) + (if mu4e~view-cited-hidden + (mu4e-view-refresh) + (mu4e~view-hide-cited))) + +(defun mu4e-view-toggle-html () + "Toggle html-display of the message body (if any)." + (interactive) + (setq mu4e~view-html-text + (if mu4e~message-body-html 'text 'html)) + (mu4e-view-refresh)) + +(defun mu4e-view-refresh () + "Redisplay the current message." + (interactive) + (mu4e-view mu4e~view-message) + (setq mu4e~view-cited-hidden nil)) + +;;; Wash functions + +(defun mu4e-view-fill-long-lines () + "Fill lines that are wider than the window width or `fill-column'." + (interactive) + (with-current-buffer (mu4e-get-view-buffer) + (save-excursion + (let ((inhibit-read-only t) + (width (window-width (get-buffer-window (current-buffer))))) + (save-restriction + (message-goto-body) + (while (not (eobp)) + (end-of-line) + (when (>= (current-column) (min fill-column width)) + (narrow-to-region (min (1+ (point)) (point-max)) + (point-at-bol)) + (let ((goback (point-marker))) + (fill-paragraph nil) + (goto-char (marker-position goback))) + (widen)) + (forward-line 1))))))) + +;;; Attachment handling + +(defun mu4e~view-get-attach-num (prompt _msg &optional multi) + "Ask the user with PROMPT for an attachment number for MSG, and +ensure it is valid. The number is [1..n] for attachments +\[0..(n-1)] in the message. If MULTI is nil, return the number for +the attachment; otherwise (MULTI is non-nil), accept ranges of +attachment numbers, as per `mu4e-split-ranges-to-numbers', and +return the corresponding string." + (let* ((count (hash-table-count mu4e~view-attach-map)) (def)) + (when (zerop count) (mu4e-warn "No attachments 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-get-attach (msg attnum) + "Return the attachment plist in MSG corresponding to attachment +number ATTNUM." + (let* ((partid (gethash attnum mu4e~view-attach-map)) + (attach + (cl-find-if + (lambda (part) + (eq (mu4e-message-part-field part :index) partid)) + (mu4e-message-field msg :parts)))) + (or attach (mu4e-error "Not a valid attachment")))) + +(defun mu4e~view-request-attachment-path (fname path) + "Ask the user where to save FNAME (default is PATH/FNAME)." + (let ((fpath (expand-file-name + (read-file-name + (mu4e-format "Save as ") + path nil nil fname) path))) + (if (file-directory-p fpath) + (expand-file-name fname fpath) + fpath))) + +(defun mu4e~view-request-attachments-dir (path) + "Ask the user where to save multiple attachments (default is PATH)." + (let ((fpath (expand-file-name + (read-directory-name + (mu4e-format "Save in directory ") + path nil nil nil) path))) + (if (file-directory-p fpath) + fpath))) + +(defun mu4e-view-save-attachment-single (&optional msg attnum) + "Save attachment number ATTNUM from MSG. +If MSG is nil use the message returned by `message-at-point'. +If ATTNUM is nil ask for the attachment number." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (attnum (or attnum + (mu4e~view-get-attach-num "Attachment to save" msg))) + (att (mu4e~view-get-attach msg attnum)) + (fname (plist-get att :name)) + (mtype (plist-get att :mime-type)) + (path (concat + (mu4e~get-attachment-dir fname mtype) "/")) + (index (plist-get att :index)) + (retry t) (fpath)) + (while retry + (setq fpath (mu4e~view-request-attachment-path fname path)) + (setq retry + (and (file-exists-p fpath) + (not (y-or-n-p (mu4e-format "Overwrite '%s'?" fpath)))))) + (mu4e~proc-extract + 'save (mu4e-message-field msg :docid) + index mu4e-decryption-policy fpath))) + +(defun mu4e-view-save-attachment-multi (&optional msg) + "Offer to save multiple email attachments from the current message. +Default is to save all messages, [1..n], where n is the number of +attachments. You can type multiple values separated by space, e.g. + 1 3-6 8 +will save attachments 1,3,4,5,6 and 8. + +Furthermore, there is a shortcut \"a\" which so means all +attachments, but as this is the default, you may not need it." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (attachstr (mu4e~view-get-attach-num + "Attachment number range (or 'a' for 'all')" msg t)) + (count (hash-table-count mu4e~view-attach-map)) + (attachnums (mu4e-split-ranges-to-numbers attachstr count))) + (if mu4e-save-multiple-attachments-without-asking + (let* ((path (concat (mu4e~get-attachment-dir) "/")) + (attachdir (mu4e~view-request-attachments-dir path))) + (dolist (num attachnums) + (let* ((att (mu4e~view-get-attach msg num)) + (fname (plist-get att :name)) + (index (plist-get att :index)) + (retry t) + fpath) + (while retry + (setq fpath (expand-file-name (concat attachdir fname) path)) + (setq retry + (and (file-exists-p fpath) + (not (y-or-n-p + (mu4e-format "Overwrite '%s'?" fpath)))))) + (mu4e~proc-extract + 'save (mu4e-message-field msg :docid) + index mu4e-decryption-policy fpath)))) + (dolist (num attachnums) + (mu4e-view-save-attachment-single msg num))))) + +(defun mu4e-view-save-attachment () + "Save mime parts from current mu4e-view buffer." + (interactive) + (call-interactively #'mu4e-view-save-attachment-multi)) + +(defun mu4e-view-open-attachment (&optional msg attnum) + "Open attachment number ATTNUM from MSG. +If MSG is nil use the message returned by `message-at-point'. If +ATTNUM is nil ask for the attachment number." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (attnum (or attnum + (progn + (unless mu4e~view-attach-map + (mu4e~view-construct-attachments-header msg)) + (mu4e~view-get-attach-num "Attachment to open" msg)))) + (att (or (mu4e~view-get-attach msg attnum))) + (index (plist-get att :index)) + (docid (mu4e-message-field msg :docid)) + (mimetype (plist-get att :mime-type))) + (if (and mimetype (string= mimetype "message/rfc822")) + ;; special handling for message-attachments; we open them in mu4e. we also + ;; send the docid as parameter (4th arg); we'll get this back from the + ;; server, and use it to determine the parent message (ie., the current + ;; message) when showing the embedded message/rfc822, and return to the + ;; current message when quitting that one. + (mu4e~view-temp-action docid index 'mu4e (format "%s" docid)) + ;; otherwise, open with the default program (handled in mu-server + (mu4e~proc-extract 'open docid index mu4e-decryption-policy)))) + +(defun mu4e~view-temp-action (docid index what &optional param) + "Open attachment INDEX for message with DOCID, and invoke ACTION." + (interactive) + (mu4e~proc-extract 'temp docid index mu4e-decryption-policy nil what param )) + +(defvar mu4e~view-open-with-hist nil "History list for the open-with argument.") + +(defun mu4e-view-open-attachment-with (msg attachnum &optional cmd) + "Open MSG's attachment ATTACHNUM with CMD. +If CMD is nil, ask user for it." + (let* ((att (mu4e~view-get-attach msg attachnum)) + (ext (file-name-extension (plist-get att :name))) + (cmd (or cmd + (read-string + (mu4e-format "Shell command to open it with: ") + (assoc-default ext mu4e-view-attachment-assoc) + 'mu4e~view-open-with-hist))) + (index (plist-get att :index))) + (mu4e~view-temp-action + (mu4e-message-field msg :docid) index 'open-with cmd))) + +(defvar mu4e~view-pipe-hist nil + "History list for the pipe argument.") + +(defun mu4e-view-pipe-attachment (msg attachnum &optional pipecmd) + "Feed MSG's attachment ATTACHNUM through pipe PIPECMD. +If PIPECMD is nil, ask user for it." + (let* ((att (mu4e~view-get-attach msg attachnum)) + (pipecmd (or pipecmd + (read-string + (mu4e-format "Pipe: ") + nil + 'mu4e~view-pipe-hist))) + (index (plist-get att :index))) + (mu4e~view-temp-action + (mu4e-message-field msg :docid) index 'pipe pipecmd))) + +(defun mu4e-view-open-attachment-emacs (msg attachnum) + "Open MSG's attachment ATTACHNUM in the current emacs instance." + (let* ((att (mu4e~view-get-attach msg attachnum)) + (index (plist-get att :index))) + (mu4e~view-temp-action (mu4e-message-field msg :docid) index 'emacs))) + +(defun mu4e-view-import-attachment-diary (msg attachnum) + "Open MSG's attachment ATTACHNUM in the current emacs instance." + (interactive) + (let* ((att (mu4e~view-get-attach msg attachnum)) + (index (plist-get att :index))) + (mu4e~view-temp-action (mu4e-message-field msg :docid) index 'diary))) + +(defun mu4e-view-import-public-key (msg attachnum) + "Import MSG's attachment ATTACHNUM into the gpg-keyring." + (interactive) + (let* ((att (mu4e~view-get-attach msg attachnum)) + (index (plist-get att :index)) + (mime-type (plist-get att :mime-type))) + (if (string= "application/pgp-keys" mime-type) + (mu4e~view-temp-action (mu4e-message-field msg :docid) index 'gpg) + (mu4e-error "Invalid mime-type for a pgp-key: `%s'" mime-type)))) + +(defun mu4e-view-attachment-action (&optional msg) + "Ask user what to do with attachments in MSG +If MSG is nil use the message returned by `message-at-point'. +The actions are specified in `mu4e-view-attachment-actions'." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (actionfunc (mu4e-read-option + "Action on attachment: " + mu4e-view-attachment-actions)) + (multi (eq actionfunc 'mu4e-view-save-attachment-multi)) + (attnum (unless multi + (mu4e~view-get-attach-num "Which attachment" msg multi)))) + (cond ((and actionfunc attnum) + (funcall actionfunc msg attnum)) + ((and actionfunc multi) + (funcall actionfunc msg))))) + +;; handler-function to handle the response we get from the server when we +;; want to do something with one of the attachments. +(defun mu4e~view-temp-handler (path what docid param) + "Handler function for doing things with temp files (ie., +attachments) in response to a (mu4e~proc-extract 'temp ... )." + (cond + ((string= what "open-with") + ;; 'param' will be the program to open-with + (start-process "*mu4e-open-with-proc*" "*mu4e-open-with*" param path)) + ((string= what "pipe") + ;; 'param' will be the pipe command, path the infile for this + (mu4e-process-file-through-pipe path param)) + ;; if it's mu4e, it's some embedded message; 'param' may contain the docid + ;; of the parent message. + ((string= what "mu4e") + ;; remember the mapping path->docid, which maps the path of the embedded + ;; message to the docid of its parent + (puthash path docid mu4e~path-parent-docid-map) + (mu4e~proc-view-path path mu4e-view-show-images mu4e-decryption-policy)) + ((string= what "emacs") + (find-file path) + ;; make the buffer read-only since it usually does not make + ;; sense to edit the temp buffer; use C-x C-q if you insist... + (setq buffer-read-only t)) + ((string= what "diary") + (icalendar-import-file path diary-file)) + ((string= what "gpg") + (epa-import-keys path)) + (t (mu4e-error "Unsupported action %S" what)))) + + +;;; Various commands + +(defconst mu4e~verify-buffer-name " *mu4e-verify*") + +(defun mu4e-view-verify-msg-popup (&optional msg) + "Pop-up a signature verification window for MSG. +If MSG is nil, use the message at point." + (interactive) + (let* ((msg (or msg (mu4e-message-at-point))) + (path (mu4e-message-field msg :path)) + (cmd (format "%s verify --verbose %s %s" + mu4e-mu-binary + (shell-quote-argument path) + (if mu4e-decryption-policy + "--decrypt --use-agent" + ""))) + (output (shell-command-to-string cmd)) + ;; create a new one + (buf (get-buffer-create mu4e~verify-buffer-name)) + (win (or (get-buffer-window buf) + (split-window-vertically (- (window-height) 6))))) + (with-selected-window win + (let ((inhibit-read-only t)) + ;; (set-window-dedicated-p win t) + (switch-to-buffer buf) + (erase-buffer) + (insert output) + (goto-char (point-min)) + (local-set-key "q" 'kill-buffer-and-window)) + (setq buffer-read-only t)) + (select-window win))) + + +;; Actions that are only available for the old view + +;;; To HTML + +(defun mu4e~action-header-to-html (msg field) + "Convert the FIELD of MSG to an HTML string." + (mapconcat + (lambda(c) + (let* ((name (when (car c) + (replace-regexp-in-string "[[:cntrl:]]" "" (car c)))) + (email (when (cdr c) + (replace-regexp-in-string "[[:cntrl:]]" "" (cdr c)))) + (addr (if mu4e-view-show-addresses + (if name (format "%s <%s>" name email) email) + (or name email))) ;; name may be nil + ;; Escape HTML entities + (addr (replace-regexp-in-string "&" "&" addr)) + (addr (replace-regexp-in-string "<" "<" addr)) + (addr (replace-regexp-in-string ">" ">" addr))) + addr)) + (mu4e-message-field msg field) ", ")) + +(defun mu4e~write-body-to-html (msg) + "Write MSG's body (either html or text) to a temporary file; +return the filename." + (let* ((html (mu4e-message-field msg :body-html)) + (txt (mu4e-message-field msg :body-txt)) + (tmpfile (mu4e-make-temp-file "html")) + (attachments (cl-remove-if (lambda (part) + (or (null (plist-get part :attachment)) + (null (plist-get part :cid)))) + (mu4e-message-field msg :parts)))) + (unless (or html txt) + (mu4e-error "No body part for this message")) + (with-temp-buffer + (insert "\n") + (insert (concat "

From: " + (mu4e~action-header-to-html msg :from) "
")) + (insert (concat "To: " + (mu4e~action-header-to-html msg :to) "
")) + (insert (concat "Date: " + (format-time-string mu4e-view-date-format (mu4e-message-field msg :date)) "
")) + (insert (concat "Subject: " (mu4e-message-field msg :subject) "

")) + (insert (or html (concat "
" txt "
"))) + (write-file tmpfile) + ;; rewrite attachment urls + (mapc (lambda (attachment) + (goto-char (point-min)) + (while (re-search-forward (format "src=\"cid:%s\"" + (plist-get attachment :cid)) nil t) + (if (plist-get attachment :temp) + (replace-match (format "src=\"%s\"" + (plist-get attachment :temp))) + (replace-match (format "src=\"%s%s\"" temporary-file-directory + (plist-get attachment :name))) + (let ((tmp-attachment-name + (format "%s%s" temporary-file-directory + (plist-get attachment :name)))) + (mu4e~proc-extract 'save (mu4e-message-field msg :docid) + (plist-get attachment :index) + mu4e-decryption-policy tmp-attachment-name) + (mu4e-remove-file-later tmp-attachment-name))))) + attachments) + (save-buffer) + tmpfile))) + +(defun mu4e-action-view-in-browser (msg) + "View the body of MSG in a web browser. +You can influence the browser to use with the variable +`browse-url-generic-program', and see the discussion of privacy +aspects in `(mu4e) Displaying rich-text messages'. This is only +available for the old view." + (browse-url (concat "file://" (mu4e~write-body-to-html msg)))) + +(defun mu4e-action-view-with-xwidget (msg) + "View the body of MSG inside xwidget-webkit. +This is only available in Emacs 25+; also see the discussion of +privacy aspects in `(mu4e) Displaying rich-text messages'." + (unless (fboundp 'xwidget-webkit-browse-url) + (mu4e-error "No xwidget support available")) + (xwidget-webkit-browse-url + (concat "file://" (mu4e~write-body-to-html msg)) t)) + +;;; To speech + +(defconst mu4e-text2speech-command "festival --tts" + "Program that speaks out text it receives on standard input.") + +(defun mu4e-action-message-to-speech (msg) + "Pronounce MSG's body text using `mu4e-text2speech-command'." + (unless (mu4e-message-field msg :body-txt) + (mu4e-warn "No text body for this message")) + (with-temp-buffer + (insert (mu4e-message-field msg :body-txt)) + (shell-command-on-region (point-min) (point-max) + mu4e-text2speech-command))) + +;;; +(provide 'mu4e-view-old) +;;; mu4e-view-old.el ends here diff --git a/mu4e/mu4e-view.el b/mu4e/mu4e-view.el index 6ece4196..c6ca2375 100644 --- a/mu4e/mu4e-view.el +++ b/mu4e/mu4e-view.el @@ -26,1227 +26,44 @@ ;; viewing e-mail messages ;;; Code: - -(require 'cl-lib) -(require 'calendar) -(require 'gnus-art) -(require 'comint) -(require 'browse-url) -(require 'button) -(require 'epa) -(require 'epg) -(require 'thingatpt) - -(require 'mu4e-actions) -(require 'mu4e-compose) -(require 'mu4e-context) -(require 'mu4e-headers) -(require 'mu4e-mark) -(require 'mu4e-message) -(require 'mu4e-server) -(require 'mu4e-search) -(require 'mu4e-utils) ;; utility functions -(require 'mu4e-contacts) -(require 'mu4e-vars) - -;;; 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 in browser" . mu4e-action-view-in-browser) - ("show this thread" . mu4e-action-show-thread)) - "List of actions to perform on messages in view mode. -The actions are cons-cells of the form: - (NAME . FUNC) -where: -* NAME is the name of the action (e.g. \"Count lines\") -* FUNC is a function which receives a message plist as an argument. - -The first letter of NAME is used as a shortcut character." - :group 'mu4e-view - :type '(alist :key-type string :value-type function)) - -(defcustom mu4e-view-max-specpdl-size 4096 - "The value of `max-specpdl-size' for displaying messages with Gnus." - :type 'integer - :group 'mu4e-view) - - -;;; Old options - -;; Options from the old message view. -(make-obsolete-variable 'mu4e-view-show-addresses - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-view-wrap-lines nil "0.9.9-dev7") -(make-obsolete-variable 'mu4e-view-hide-cited nil "0.9.9-dev7") -(make-obsolete-variable 'mu4e-view-date-format - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-view-image-max-width - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-view-image-max-height - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-save-multiple-attachments-without-asking - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-view-attachment-assoc - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-view-attachment-actions - "See mu4e-view-mime-part-actions" "1.7.0") -(make-obsolete-variable 'mu4e-view-header-field-keymap - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-view-header-field-keymap - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-view-contacts-header-keymap - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-view-attachments-header-keymap - "Unused with the new message view" "1.7.0") -(make-obsolete-variable 'mu4e-imagemagick-identify nil "1.7.0") -(make-obsolete-variable 'mu4e-view-show-images - "No longer used" "1.7.0") -(make-obsolete-variable 'mu4e-view-gnus "Old view is gone" "1.7.0") -(make-obsolete-variable 'mu4e-view-use-gnus "Gnus view is the default" "1.5.10") - -(make-obsolete-variable 'mu4e-cited-regexp "No longer used" "1.7.0") - - - -;; 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 (mu4e-search-narrow))) - -(defun mu4e-view-search-edit () - "Run `mu4e-headers-search-edit' in the headers buffer." - (interactive) - (mu4e~view-in-headers-context (mu4e-search-edit))) - -(defun mu4e-mark-region-code () - "Highlight region marked with `message-mark-inserted-region'. -Add this function to `mu4e-view-mode-hook' to enable this feature." - (require 'message) - (let (beg end ov-beg ov-end ov-inv) - (save-excursion - (goto-char (point-min)) - (while (re-search-forward - (concat "^" message-mark-insert-begin) nil t) - (setq ov-beg (match-beginning 0) - ov-end (match-end 0) - ov-inv (make-overlay ov-beg ov-end) - beg ov-end) - (overlay-put ov-inv 'invisible t) - (overlay-put ov-inv 'mu4e-overlay t) - (when (re-search-forward - (concat "^" message-mark-insert-end) nil t) - (setq ov-beg (match-beginning 0) - ov-end (match-end 0) - ov-inv (make-overlay ov-beg ov-end) - end ov-beg) - (overlay-put ov-inv 'invisible t)) - (when (and beg end) - (let ((ov (make-overlay beg end))) - (overlay-put ov 'mu4e-overlay t) - (overlay-put ov 'face 'mu4e-region-code)) - (setq beg nil end nil)))))) - -;;; View Utilities - -(defun mu4e-view-mark-custom () - "Run some custom mark function." - (mu4e~view-in-headers-context - (mu4e-headers-mark-custom))) - -(defun mu4e~view-split-view-p () - "Return t if we're in split-view, nil otherwise." - (member mu4e-split-view '(horizontal vertical))) - -;;; Scroll commands - -(defun mu4e-view-scroll-up-or-next () - "Scroll-up the current message. -If `mu4e-view-scroll-to-next' is non-nil, and we 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-") '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 'mu4e-overlay t) - (overlay-put ov 'after-string - (propertize (format "\u200B[%d]" num) - 'face 'mu4e-url-number-face))))))))) - - -(defun mu4e~view-get-urls-num (prompt &optional multi) - "Ask the user with PROMPT for an URL number for MSG, 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)) +(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-search (concat "msgid:" msgid) nil nil t msgid t)) + (mu4e-headers-search (concat "msgid:" msgid) nil nil t msgid t)) - -;;; Variables - -(defvar gnus-icalendar-additional-identities) -(defvar helm-comp-read-use-marked) -(defvar-local mu4e~view-rendering nil) - -(define-obsolete-variable-alias 'mu4e-view-blocked-images 'gnus-blocked-images - "1.5.12") -(define-obsolete-variable-alias 'mu4e-view-inhibit-images 'gnus-inhibit-images - "1.5.12") -;;; Main - -;; remember the mime-handles, so we can clean them up when -;; we quit this buffer. -(defvar-local mu4e~gnus-article-mime-handles nil) -(put 'mu4e~gnus-article-mime-handles 'permanent-local t) - -(defun mu4e-view (msg) - "Display the message MSG in a new buffer, and keep in sync with HDRSBUF. -'In sync' here means that moving to the next/previous message in -the the message view affects HDRSBUF, as does marking etc. - -As a side-effect, a message that is being viewed loses its 'unread' -marking if it still had that." - - (mu4e~headers-update-handler msg nil nil);; update headers, if necessary. - - (when (bufferp gnus-article-buffer) - (kill-buffer gnus-article-buffer)) - (with-current-buffer (get-buffer-create gnus-article-buffer) - (let ((inhibit-read-only t)) - (remove-overlays (point-min)(point-max) 'mu4e-overlay t) - (erase-buffer) - (insert-file-contents-literally - (mu4e-message-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 MSG as a string." - ;; we need this for replying/forwarding, since the mu4e-compose - ;; wants it that way. - (with-temp-buffer - (insert-file-contents-literally - (mu4e-message-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 MSG in browser if it includes an HTML-part. -The variables `browse-url-browser-function', -`browse-url-handlers', and `browse-url-default-handlers' -determine which browser function to use." - (with-temp-buffer - (insert-file-contents-literally - (mu4e-message-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." - (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) - "Like `gnus-display-mime' but include mu4e headers to MSG." - (lambda (&optional ihandles) - (gnus-display-mime ihandles) - (unless ihandles - (save-restriction - (article-goto-body) - (forward-line -1) - (narrow-to-region (point) (point)) - (dolist (field mu4e-view-fields) - (let ((fieldval (mu4e-message-field msg field))) - (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." - (let* ((info (cdr (assoc field mu4e-header-info))) - (key (plist-get info :name)) - (help (plist-get info :help))) - (if (and val (> (length val) 0)) - (insert (propertize (concat key ":") 'help-echo help) - " " val "\n")))) - -(defun mu4e~view-gnus-insert-header-custom (msg field) - "Insert MSG's custom FIELD." - (let* ((info (cdr-safe (or (assoc field mu4e-header-info-custom) - (mu4e-error "Custom field %S not found" field)))) - (key (plist-get info :name)) - (func (or (plist-get info :function) - (mu4e-error "No :function defined for custom field %S %S" - field info))) - (val (funcall func msg)) - (help (plist-get info :help))) - (when (and val (> (length val) 0)) - (insert (propertize (concat key ":") 'help-echo help) " " val "\n")))) - -(define-advice gnus-icalendar-event-from-handle - (:filter-args (handle-attendee) mu4e~view-fix-missing-charset) - "Avoid error when displaying an ical attachment without a charset." - (if (and (boundp 'mu4e~view-rendering) mu4e~view-rendering) - (let* ((handle (car handle-attendee)) - (attendee (cadr handle-attendee)) - (buf (mm-handle-buffer handle)) - (ty (mm-handle-type handle)) - (rest (cddr handle))) - ;; Put the fallback at the end: - (setq ty (append ty '((charset . "utf-8")))) - (setq handle (cons buf (cons ty rest))) - (list handle attendee)) - handle-attendee)) - -(defun mu4e~view-mode-p () - "Is the buffer in mu4e-view-mode or one of its descendants?" - (or (eq major-mode 'mu4e-view-mode) - (derived-mode-p '(mu4e-view-mode)))) - -(defun mu4e~view-nop (func &rest args) - "Do not invoke FUNC with ARGS when in mu4e-view-mode. -This is useful for advising some Gnus-functionality that does not work in mu4e." - (unless (mu4e~view-mode-p) - (apply func args))) - -(defun mu4e~view-button-reply (func &rest args) - "Advise FUNC with ARGS to make `gnus-button-reply' links work in mu4e." - (if (mu4e~view-mode-p) - (mu4e-compose-reply) - (apply func args))) - -(defun mu4e~view-msg-mail (func &rest args) - "Advise FUNC with ARGS to make `gnus-msg-mail' links compose with mu4e." - (if (mu4e~view-mode-p) - (apply 'mu4e~compose-mail args) - (apply func args))) - -(defvar mu4e-view-mode-map - (let ((map (make-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 "%" #'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) - - ;; 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 "") #'mu4e-headers-split-view-grow) - (define-key map (kbd "") #'mu4e-headers-split-view-shrink) - - ;; intra-message navigation - (define-key map (kbd "S-SPC") #'scroll-down) - (define-key map (kbd "SPC") #'mu4e-view-scroll-up-or-next) - (define-key map (kbd "RET") #'mu4e-scroll-up) - (define-key map (kbd "") #'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 "") #'mu4e-view-headers-next) - (define-key map (kbd "") #'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 "") #'mu4e-view-mark-for-delete) - (define-key map (kbd "") #'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 "") #'mu4e-view-mark-for-something) - (define-key map (kbd "") #'mu4e-view-mark-for-something) - (define-key map (kbd "") #'mu4e-view-mark-for-something) - - (define-key map (kbd "#") #'mu4e-mark-resolve-deferred-marks) - ;; misc - (define-key map "M" #'mu4e-view-massage) - - (define-key map "w" 'visual-line-mode) - (define-key map "h" #'mu4e-view-toggle-html) - (define-key map (kbd "M-q") 'article-fill-long-lines) - - ;; 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) - -;; "Define the major-mode for the mu4e-view." -(define-derived-mode mu4e-view-mode gnus-article-mode "mu4e:view" - "Major mode for viewing an e-mail message in mu4e. -Based on Gnus' article-mode." - ;; Restore C-h b default behavior - (define-key mu4e-view-mode-map (kbd "C-h b") 'describe-bindings) - ;; ;; turn off gnus modeline changes and menu items - (advice-add 'gnus-set-mode-line :around #'mu4e~view-nop) - (advice-add 'gnus-button-reply :around #'mu4e~view-button-reply) - (advice-add 'gnus-msg-mail :around #'mu4e~view-msg-mail) - - ;; advice gnus-block-private-groups to always return "." - ;; so that by default we block images. - (advice-add 'gnus-block-private-groups :around - (lambda(func &rest args) - (if (mu4e~view-mode-p) - "." (apply func args)))) - (use-local-map mu4e-view-mode-map) - (mu4e-context-minor-mode) - (mu4e-search-minor-mode) - (setq buffer-undo-list t);; don't record undo info - ;; autopair mode gives error when pressing RET - ;; turn it off - (when (boundp 'autopair-dont-activate) - (setq autopair-dont-activate t))) - -;;; Massaging the message view - -(defcustom mu4e-view-massage-options - '( ("ctoggle citations" . gnus-article-hide-citation) - ("htoggle headers" . gnus-article-hide-headers) - ("ytoggle crypto" . gnus-article-hide-pem)) -"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. -The alist uniquely maps the number to the gnus-part." - (let ((parts '())) - (save-excursion - (goto-char (point-min)) - (while (not (eobp)) - (let ((part (get-text-property (point) 'gnus-data)) - (index (get-text-property (point) 'gnus-part))) - (when (and part (numberp index) (not (assoc index parts)) - (push `(,index . ,part) parts))) - (goto-char (or (next-single-property-change (point) 'gnus-part) - (point-max)))))) - parts)) - - -(defun mu4e-view-save-attachments (&optional arg) - "Save mime parts from current mu4e gnus view buffer. - -When helm-mode is enabled provide completion on attachments and -possibility to mark candidates to save, otherwise completion on -attachments is done with `completing-read-multiple', in this case -use \",\" to separate candidate, completion is provided after -each \",\". - -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 (let ((file (expand-file-name f dir))) - (if (file-exists-p file) - (let (newname (count 1)) - (while (and - (setq newname - (concat - (file-name-sans-extension file) - (format "(%s)" count) - (file-name-extension file t))) - (file-exists-p newname)) - (cl-incf count)) - newname) - file))))) - (mu4e-message "No attached files found")))) - - -(defvar mu4e-view-mime-part-actions - '( - ;; - ;; some basic ones - ;; - - ;; save MIME-part to a file - (:name "save" :handler gnus-article-save-part :receives index) - ;; pipe MIME-part to some arbitrary shell command - (:name "|pipe" :handler gnus-article-pipe-part :receives index) - ;; open with the default handler, if any - (:name "open" :handler mu4e~view-open-file :receives temp) - ;; open with some custom file. - (:name "wopen-with" :handler (lambda (file)(mu4e~view-open-file file t)) - :receives temp) - - ;; - ;; some more examples - ;; - - ;; import GPG key - (:name "gpg" :handler epa-import-keys :receives temp) - ;; count the number of lines in a MIME-part - (:name "line-count" :handler "wc -l" :receives pipe) - ;; open in this emacs instance; tries to use the attachment name, - ;; so emacs can use specific modes etc. - (:name "emacs" :handler find-file :receives temp) - ;; open in this emacs instance, "raw" - (:name "raw" :handler (lambda (str) - (let ((tmpbuf (get-buffer-create " *mu4e-raw-mime*"))) - (with-current-buffer tmpbuf - (insert str) - (view-mode) - (goto-char (point-min))) - (switch-to-buffer tmpbuf))) :receives pipe)) - - "Specifies actions for MIME-parts. - -Each of the actions is a plist with keys -`(:name ;; name of the action; shortcut is first letter of name - - :handler ;; one of: - ;; - a function receiving the index/temp/pipe - ;; - a string, which is taken as a shell command - - :receives ;; a symbol specifying what the handler receives - ;; - index: the index number of the mime part (default) - ;; - temp: the full path to the mime part in a - ;; temporary file, which is deleted immediately - ;; after invoking handler - ;; - pipe: the attachment is piped to some shell command - ;; or as a string parameter to a function -).") - - -(defun mu4e~view-mime-part-to-temp-file (handle) - "Write MIME-part HANDLE to a temporary file and return the file name. -The filename is deduced from the MIME-part's filename, or -otherwise random; the result is placed in a temporary directory -with a unique name. Returns the full path for the file created. -The directory and file are self-destructed." - (let* ((tmpdir (make-temp-file "mu4e-temp-" t)) - (fname (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 to MIME-part N in the current messsage. -If N is not specified, ask for it. For instance, '3 A o' opens -the third MIME-part." - (interactive "NNumber of MIME-part: ") - (let* ((parts (mu4e~view-gather-mime-parts)) - (options (mapcar (lambda (action) `(,(plist-get action :name) . ,action)) - mu4e-view-mime-part-actions)) - (handle (or (cdr-safe (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 item found for action %S" action))) - (receives (or (plist-get action :receives) - (mu4e-error "No :receives item found for action %S" action)))) - (save-excursion - (cond - ((functionp handler) - (cond - ((eq receives 'index) (funcall handler n)) - ((eq receives 'pipe) (funcall handler (mm-with-unibyte-buffer - (mm-insert-part handle) - (buffer-string)))) - ((eq receives 'temp) - (funcall handler (mu4e~view-mime-part-to-temp-file handle))) - (t (mu4e-error "Invalid :receive for %S" action)))) - ((stringp handler) - (cond - ((eq receives 'index) (shell-command (concat handler " " (shell-quote-argument n)))) - ((eq receives 'pipe) (mm-pipe-part handle handler)) - ((eq receives 'temp) - (shell-command (shell-command (concat handler " " - (shell-quote-argument - (mu4e~view-mime-part-to-temp-file handle)))))) - (t (mu4e-error "Invalid action %S" action)))))))) - -(defun mu4e-view-toggle-html () - "Toggle html-display of the first html-part found." - (interactive) - ;; This function assumes `gnus-article-mime-handle-alist' is sorted by - ;; pertinence, i.e. the first HTML part found in it is the most important one. - (if-let ((html-part - (seq-find (lambda (handle) - (equal (mm-handle-media-type (cdr handle)) "text/html")) - gnus-article-mime-handle-alist))) - (gnus-article-inline-part (car html-part)) - (mu4e-warn "No html part in this message"))) - -(defun mu4e-process-file-through-pipe (path pipecmd) - "Process file at PATH through a pipe with PIPECMD." - (let ((buf (get-buffer-create "*mu4e-output"))) - (with-current-buffer buf - (let ((inhibit-read-only t)) - (erase-buffer) - (call-process-shell-command pipecmd path t t) - (view-mode))) - (switch-to-buffer buf))) - -;;; Bug Reference mode support - -;; This is Emacs 28 stuff but there is no need to guard it with some (f)boundp -;; checks (which would return nil if bug-reference.el is not loaded before -;; mu4e) since the function definition doesn't hurt and `add-hook' works fine -;; for not yet defined variables (by creating them). -(declare-function bug-reference-maybe-setup-from-mail "ext:bug-reference") -(defun mu4e--view-try-setup-bug-reference-mode () - "Try to guess bug-reference setup from the current mu4e mail. -Looks at the maildir and the mail headers List, List-Id, Maildir, -To, From, Cc, and Subject and tries to guess suitable values for -`bug-reference-bug-regexp' and `bug-reference-url-format' by -matching the maildir name against GROUP-REGEXP and each header -value against HEADER-REGEXP in -`bug-reference-setup-from-mail-alist'." - (when (derived-mode-p 'mu4e-view-mode) - (let (header-values) - (save-excursion - (goto-char (point-min)) - (dolist (field '("list" "list-id" "to" "from" "cc" "subject")) - (let ((val (mail-fetch-field field))) - (when val - (push val header-values))))) - (bug-reference-maybe-setup-from-mail - (mail-fetch-field "maildir") - header-values)))) - -(add-hook 'bug-reference-auto-setup-functions - #'mu4e--view-try-setup-bug-reference-mode) - - (provide 'mu4e-view) ;;; mu4e-view.el ends here diff --git a/mu4e/mu4e.el b/mu4e/mu4e.el index 94217571..a28aeab3 100644 --- a/mu4e/mu4e.el +++ b/mu4e/mu4e.el @@ -1,6 +1,6 @@ ;;; mu4e.el --- part of mu4e, the mu mail user agent -*- lexical-binding: t -*- -;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema +;; Copyright (C) 2011-2019 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema ;; Maintainer: Dirk-Jan C. Binnema @@ -27,33 +27,13 @@ ;;; Code: (require 'mu4e-vars) -(require 'mu4e-helpers) -(require 'mu4e-folders) -(require 'mu4e-context) -(require 'mu4e-contacts) -(require 'mu4e-headers) -(require 'mu4e-compose) -(require 'mu4e-bookmarks) -(require 'mu4e-update) -(require 'mu4e-main) -(require 'mu4e-server) ;; communication with backend - - - -(defcustom mu4e-confirm-quit t - "Whether to confirm to quit mu4e." - :type 'boolean - :group 'mu4e) - -(defcustom mu4e-org-support t - "Support Org-mode links." - :type 'boolean - :group 'mu4e) - -(defcustom mu4e-speedbar-support nil - "Support having a speedbar to navigate folders/bookmarks." - :type 'boolean - :group 'mu4e) +(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 @@ -68,177 +48,20 @@ ;;;###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." + "If mu4e is not running yet, start it. Then, show the main +window, unless BACKGROUND (prefix-argument) is non-nil." (interactive "P") ;; start mu4e, then show the main view - (mu4e--init-handlers) - (mu4e--start (unless background 'mu4e~main-view))) + (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))) - -;;; Internals + (mu4e~stop)) + (mu4e~stop))) -(defun mu4e--check-requirements () - "Check for the settings required for running mu4e." - (unless (>= emacs-major-version 25) - (mu4e-error "Emacs >= 25.x is required for mu4e")) - (when (mu4e-server-properties) - (unless (string= (mu4e-server-version) mu4e-mu-version) - (mu4e-error "The mu server has version %s, but we need %s" - (mu4e-server-version) mu4e-mu-version))) - (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary)) - (mu4e-error "Please set `mu4e-mu-binary' to the full path to the mu - binary")) - (dolist (var '(mu4e-sent-folder mu4e-drafts-folder - mu4e-trash-folder)) - (unless (and (boundp var) (symbol-value var)) - (mu4e-error "Please set %S" var)) - (unless (functionp (symbol-value var)) ;; functions are okay, too - (let* ((dir (symbol-value var)) - (path (concat (mu4e-root-maildir) dir))) - (unless (string= (substring dir 0 1) "/") - (mu4e-error "%S must start with a '/'" dir)) - (unless (mu4e-create-maildir-maybe path) - (mu4e-error "%s (%S) does not exist" path var)))))) - -;;; Starting / getting mail / updating the index - -(defun mu4e--pong-handler (_data func) - "Handle 'pong' responses from the mu server. -Invoke FUNC if non-nil." - (let ((doccount (plist-get (mu4e-server-properties) :doccount))) - (mu4e--check-requirements) - (when func (funcall func)) - (when (zerop doccount) - (mu4e-message "Store is empty; (re)indexing. This may take a while.") ; - (mu4e-update-index)) - (when (and mu4e-update-interval (null mu4e--update-timer)) - (setq mu4e--update-timer - (run-at-time 0 mu4e-update-interval - (lambda () (mu4e-update-mail-and-index - mu4e-index-update-in-background))))))) - -(defun mu4e--start (&optional func) - "Start mu4e. -If `mu4e-contexts' have been defined, but we don't have a context -yet, switch to the matching one, or none matches, the first. If -mu4e is already running, execute function FUNC (if non-nil). -Otherwise, check various requireme`'nts, then start mu4e. When -successful, call FUNC (if non-nil) afterwards." - (unless (mu4e-context-current) - (mu4e--context-autoswitch nil mu4e-context-policy)) - (setq mu4e-pong-func (lambda (info) (mu4e--pong-handler info func))) - (mu4e--server-ping - (mapcar ;; send it a list of queries we'd like to see read/unread info for - (lambda (bm) - (funcall (or mu4e-search-query-rewrite-function #'identity) - (plist-get bm :query))) - ;; exclude bookmarks that are not strings, and with certain flags - (seq-filter (lambda (bm) - (and (stringp (plist-get bm :query)) - (not (or (plist-get bm :hide) - (plist-get bm :hide-unread))))) - (append (mu4e-bookmarks) - (mu4e--maildirs-with-query))))) - ;; maybe request the list of contacts, automatically refreshed after - ;; reindexing - (unless mu4e--contacts-hash (mu4e--request-contacts-maybe))) - -(defun mu4e--stop () - "Stop mu4e." - (when mu4e--update-timer - (cancel-timer mu4e--update-timer) - (setq mu4e--update-timer nil)) - (mu4e-clear-caches) - (mu4e--server-kill) - ;; kill all mu4e buffers - (mapc - (lambda (buf) - ;; When using the Gnus-based viewer, the view buffer has the - ;; kill-buffer-hook function mu4e~view-kill-buffer-hook-fn which kills the - ;; mm-* buffers created by Gnus' article mode. Those have been returned by - ;; `buffer-list' but might already be deleted in case the view buffer has - ;; been killed first. So we need a `buffer-live-p' check here. - (when (buffer-live-p buf) - (with-current-buffer buf - (when (member major-mode - '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode)) - (kill-buffer))))) - (buffer-list))) - -;;; Handlers -(defun mu4e--error-handler (errcode errmsg) - "Handler function for showing an error with ERRCODE and ERRMSG." - ;; don't use mu4e-error here; it's running in the process filter context - (cl-case errcode - (4 (mu4e-warn "No matches for this search query.")) - (110 (display-warning 'mu4e errmsg :error)) ;; schema version. - (t (error "Error %d: %s" errcode errmsg)))) - - -(defun mu4e--info-handler (info) - "Handler function for (:INFO ...) sexps received from server." - (let* ((type (plist-get info :info)) - (processed (plist-get info :processed)) - (updated (plist-get info :updated)) - (cleaned-up (plist-get info :cleaned-up)) - (mainbuf (get-buffer mu4e-main-buffer-name))) - (cond - ((eq type 'add) t) ;; do nothing - ((eq type 'index) - (if (eq (plist-get info :status) 'running) - (mu4e-index-message - "Indexing... processed %d, updated %d" processed updated) - (progn - (mu4e-index-message - "%s completed; processed %d, updated %d, cleaned-up %d" - (if mu4e-index-lazy-check "Lazy indexing" "Indexing") - processed updated cleaned-up) - ;; call the updated hook if anything changed. - (unless (zerop (+ updated cleaned-up)) - (run-hooks 'mu4e-index-updated-hook)) - (unless (and (not (string= mu4e--contacts-tstamp "0")) - (zerop (plist-get info :updated))) - (mu4e--request-contacts-maybe)) - (when (and (buffer-live-p mainbuf) (get-buffer-window mainbuf)) - (save-window-excursion - (select-window (get-buffer-window mainbuf)) - (mu4e~main-view 'refresh)))))) - ((plist-get info :message) - (mu4e-index-message "%s" (plist-get info :message)))))) - -(defun mu4e--init-handlers() - "Initialize the server message handlers. -Only set set them if they were nil before, so overriding has a -chance." - (mu4e-setq-if-nil mu4e-error-func #'mu4e--error-handler) - (mu4e-setq-if-nil mu4e-update-func #'mu4e~headers-update-handler) - (mu4e-setq-if-nil mu4e-remove-func #'mu4e~headers-remove-handler) - (mu4e-setq-if-nil mu4e-view-func #'mu4e~headers-view-handler) - (mu4e-setq-if-nil mu4e-header-func #'mu4e~headers-header-handler) - (mu4e-setq-if-nil mu4e-found-func #'mu4e~headers-found-handler) - (mu4e-setq-if-nil mu4e-erase-func #'mu4e~headers-clear) - - (mu4e-setq-if-nil mu4e-sent-func #'mu4e--default-handler) - (mu4e-setq-if-nil mu4e-compose-func #'mu4e~compose-handler) - (mu4e-setq-if-nil mu4e-contacts-func #'mu4e--update-contacts) - (mu4e-setq-if-nil mu4e-info-func #'mu4e--info-handler) - (mu4e-setq-if-nil mu4e-pong-func #'mu4e--default-handler)) - -(defun mu4e-clear-caches () - "Clear any cached resources." - (setq - mu4e-maildir-list nil - mu4e--contacts-hash nil - mu4e--contacts-tstamp "0")) ;;; _ (provide 'mu4e) ;;; mu4e.el ends here diff --git a/mu4e/mu4e.texi b/mu4e/mu4e.texi index 7965407f..2806c5dd 100644 --- a/mu4e/mu4e.texi +++ b/mu4e/mu4e.texi @@ -266,20 +266,38 @@ details depend on your distribution. If you're using another distribution (or another OS), the below can at least be helpful in identifying the packages to install. -We provide some instructions for Debian, Ubuntu and Fedora; if those -do not apply to you, you can follow either @ref{Building from a -release tarball} or @ref{Building from git}. +We provide some instructions for Debian, Ubuntu and Fedora; if those do not +apply to you, you can follow either @ref{Building from a release tarball} or +@ref{Building from git}. @subsection Dependencies for Debian/Ubuntu @example -$ sudo apt-get install libgmime-3.0-dev libxapian-dev emacs +$ sudo apt-get install libgmime-3.0-dev libxapian-dev + +# get emacs 25 or higher if you don't have it yet +$ sudo apt-get install emacs + +# optional +$ sudo apt-get install guile-2.2-dev html2text xdg-utils + +# optional: only needed for msg2pdf and mug (toy gtk+ frontend) +$ sudo apt-get install libwebkitgtk-3.0-dev @end example @subsection Dependencies for Fedora @example -$ sudo yum install gmime30-devel xapian-core-devel emacs +$ sudo yum install gmime30-devel xapian-core-devel + +# get emacs 25 or higher if you don't have it yet +$ sudo yum install emacs + +# optional +$ sudo yum install html2text xdg-utils guile22-devel + +# optional: only needed for msg2pdf and mug (toy gtk+ frontend) +$ sudo yum install webkitgtk3-devel @end example @subsection Building on Msys2 @@ -330,18 +348,10 @@ Xapian, GMime and their dependencies must be installed. @subsection Building from git @anchor{Building from git} -By default, @t{mu} use the Meson@footnote{@url{https://mesonbuild.com/}} build-system. - -@example -$ git clone git://github.com/djcb/mu.git -$ cd mu -$ meson build && ninja -C build -$ sudo ninja -C install -@end example - -For now, you can also use the (deprecated) @t{autotools} build setup, -assuming you have autotools (@t{autoconf}, @t{automake}, @t{libtool}, -@t{texinfo}) installed: +Alternatively, if you build from the git repository or use a tarball +like the ones that @t{github} produces, the instructions are slightly +different, and require you to have autotools (@t{autoconf}, +@t{automake}, @t{libtool}, @t{texinfo}) installed: @example # get from git (alternatively, use a github tarball) @@ -358,18 +368,29 @@ $ sudo make install After this, @t{mu} and @t{mu4e} should be installed @footnote{there's a hard dependency between versions of @t{mu4e} and @t{mu} --- you cannot combine different versions} on your system, and be available -from the command line and in Emacs. +from the command line and in Emacs. You may need to restart Emacs, so it can find @t{mu4e} in its @code{load-path}. If, even after restarting, Emacs cannot find -@t{mu4e}, you may need to add it to your @code{load-path} explicitly; -check where @t{mu4e} is installed, and add something like the -following to your configuration before trying again: +@t{mu4e}, you may need to add it to your @code{load-path} explicitly; check +where @t{mu4e} is installed, and add something like the following to your +configuration before trying again: @lisp ;; the exact path may differ --- check it (add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e") @end lisp +@subsection Building using the meson build system + +As an (experimental) alternative to the @t{autotools}-build, it is possible to use +the Meson@footnote{@url{https://mesonbuild.com/}} build-system instead. + +@example +$ git clone git://github.com/djcb/mu.git +$ cd mu +$ meson build && ninja -C build +$ sudo ninja -C install +@end example @subsection mu4e and emacs customization @@ -1005,6 +1026,7 @@ E edit (only allowed for draft messages) misc ---- +; switch context a execute some custom action on a header | pipe message through shell command C-+,C-- increase / decrease the number of headers shown @@ -1013,10 +1035,6 @@ C-S-u update mail & reindex q leave the headers buffer @end verbatim -Furthermore, a number of keybindings are available through minor modes: -@itemize -@item Context; see @pxref{Contexts}. -@end itemize @node HV Marking @section Marking @@ -1303,6 +1321,7 @@ A execute some custom action on the message's MIME-parts misc ---- +; switch context . show the raw message view. 'q' takes you back. C-+,C-- increase / decrease the number of headers shown H get help @@ -1310,11 +1329,6 @@ C-S-u update mail & reindex q leave the message view @end verbatim -Furthermore, a number of keybindings are available through minor modes: -@itemize -@item Context; see @pxref{Contexts}. -@end itemize - For the marking commands, please refer to @ref{Marking messages}. @node MSGV Rich-text and images @@ -2287,7 +2301,7 @@ loading @t{mu4e}): ;; must come before proc-move since retag runs ;; 'sed' on the file (mu4e-action-retag-message msg "-\\Inbox") - (mu4e--server-move docid nil "+S-u-N")))) + (mu4e~proc-move docid nil "+S-u-N")))) @end lisp Adding to @code{mu4e-marks} list allows to use the mark in bulk operations @@ -5058,15 +5072,15 @@ to provide this information (this is implemented in @file{mu-cmd-server.c}). We start this sequence when @t{mu4e} is invoked (when the program is -started). It calls @t{mu4e-server-ping}, and registers a (lambda) function for -@t{mu4e-server-pong-func}, to handle the response. +started). It calls @t{mu4e-proc-ping}, and registers a (lambda) function for +@t{mu4e-proc-pong-func}, to handle the response. @verbatim -> (ping) <-(:pong "mu" :props (:version "x.x.x" :doccount 78545)) @end verbatim -When we receive such a @t{pong} (in @file{mu4e-server.el}), the lambda +When we receive such a @t{pong} (in @file{mu4e-proc.el}), the lambda function we registered is called, and it compares the version we got from the @t{pong} with the version we expected, and raises an error if they differ.