summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS.org5
-rw-r--r--mu4e/meson.build11
-rw-r--r--mu4e/mu4e-dbus.el240
-rw-r--r--mu4e/mu4e.el2
-rw-r--r--mu4e/mu4e.texi69
5 files changed, 325 insertions, 2 deletions
diff --git a/NEWS.org b/NEWS.org
index c954922..15cfac2 100644
--- a/NEWS.org
+++ b/NEWS.org
@@ -312,6 +312,11 @@
- 1.12.14: add ~mu4e-quit-hook~, a hook is just before quitting mu4e. See its
docstring for details.
+ - 1.12.14: add ~mu4e-dbus-mode~, an experimental minor mode which, if enabled on an Emacs
+ with DBus-support, exposes some of Mu4e's state (such as the the
+ read/unread counts of bookmarked queries) on the DBus session bus.
+ See [[info:mu4e#DBus
+ service][DBus service]] in the mu4e manual for further details.
*** scm
- 1.12.12: add new guile/scheme binding in ~scm/~. These are to replace the
diff --git a/mu4e/meson.build b/mu4e/meson.build
index f117f79..5f0da4d 100644
--- a/mu4e/meson.build
+++ b/mu4e/meson.build
@@ -48,7 +48,6 @@ mu4e_srcs=[
'mu4e-contrib.el',
'mu4e-draft.el',
'mu4e-folders.el',
- 'mu4e.el',
'mu4e-headers.el',
'mu4e-helpers.el',
'mu4e-icalendar.el',
@@ -70,7 +69,8 @@ mu4e_srcs=[
'mu4e-update.el',
'mu4e-vars.el',
'mu4e-view.el',
- 'mu4e-window.el'
+ 'mu4e-window.el',
+ 'mu4e.el'
]
# emacs 28 is guaranteed to have transient
@@ -80,6 +80,13 @@ if emacs28.found()
mu4e_srcs += 'mu4e-transient.el'
endif
+# do have we have dbus support?
+emacs_dbus = run_command(emacs, '--batch', '--eval', '(kill-emacs (if (featurep \'dbusbind) 0 1))',
+ check: false)
+if emacs_dbus.returncode() == 0
+ mu4e_srcs += 'mu4e-dbus.el'
+endif
+
# note, we cannot compile mu4e-config.el without incurring
# WARNING: Source item
# '[...]/build/mu4e/mu4e-meta.el' cannot be converted to File object, because
diff --git a/mu4e/mu4e-dbus.el b/mu4e/mu4e-dbus.el
new file mode 100644
index 0000000..ba06413
--- /dev/null
+++ b/mu4e/mu4e-dbus.el
@@ -0,0 +1,240 @@
+;;; mu4e-dbus.el --- DBus support -*- lexical-binding: t-*-
+
+;; Copyright (C) 2025 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; SPDX-License-Identifier: GPL-3.0-or-later
+
+;; mu4e is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; mu4e is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;; Generic support for showing new-mail notifications.
+
+;;; Code:
+
+(eval-when-compile (require 'dbus nil 'noerror))
+(require 'dbus nil 'noerror)
+
+(require 'mu4e-helpers)
+(require 'mu4e-query-items)
+(require 'mu4e-context)
+
+(defconst mu4e-dbus-service "nl.djcbsoftware.Mu4e"
+ "Mu4e's DBus service name.")
+
+(defconst mu4e-dbus-interface "nl.djcbsoftware.Mu4e"
+ "Mu4e's DBus interface.")
+
+(defconst mu4e-dbus-object-path "/nl/djcbsoftware/Mu4e"
+ "Mu4e's DBus object path.")
+;; We could use different object paths for allowing multiple mu4e's. But let's
+;; not add that complication for now.
+
+(defconst mu4e-dbus-introspection-xml
+ "<node>
+ <interface name='org.freedesktop.DBus.Introspectable'>
+ <method name='Introspect'>
+ <arg name='xml_data' type='s' direction='out'/>
+ </method>
+ </interface>
+ <interface name='org.freedesktop.DBus.Properties'>
+ <method name='Get'>
+ <arg type='s' name='interface_name' direction='in'/>
+ <arg type='s' name='property_name' direction='in'/>
+ <arg type='v' name='value' direction='out'/>
+ </method>
+ <method name='GetAll'>
+ <arg type='s' name='interface_name' direction='in'/>
+ <arg type='a{sv}' name='properties' direction='out'/>
+ </method>
+ <method name='Set'>
+ <arg type='s' name='interface_name' direction='in'/>
+ <arg type='s' name='property_name' direction='in'/>
+ <arg type='v' name='value' direction='in'/>
+ </method>
+ <signal name='PropertiesChanged'>
+ <arg type='s' name='interface_name'/>
+ <arg type='a{sv}' name='changed_properties'/>
+ <arg type='as' name='invalidated_properties'/>
+ </signal>
+ </interface>
+ <interface name='nl.djcbsoftware.Mu4e'>
+ <property name='Version' type='s' access='read'/>
+ <property name='DatabasePath' type='s' access='read'/>
+ <property name='RootMaildir' type='s' access='read'/>
+ <property name='Context' type='s' access='readwrite'/>
+ <property name='QueryInfo' type='aa{sv}' access='readwrite'/>
+ </interface>
+</node>"
+ "The XML blob used for DBus introspection.
+
+Apart from the properties boilerplate, the
+\"nl.djcbsoftware.Mu4e\" interface supports the following:
+
+1) properties:
+ - Version (string): the mu/mu4e version
+ - Context (string): the current mu4e context, if any
+ - QueryInfo (array-of-vardict):
+ information about bookmarks / queries, i.e., a
+ subset of the `mu4e-query-items' information.
+
+ Each item describes a bookmark/maildir item with
+ entries:
+ - name (string) -> name of the item
+ - query (string) -> query for this item
+ - count (number) -> number of matching messages
+ - unread (number) -> the number of unread messages
+ - favorite (bool) -> this is the favorite entry
+ (optional, at most one entry has this).")
+
+;; Introspection... although not strictly required, it is very useful for
+;; clients (including tools like d-spy) to find out the methods we support. We
+;; need to implement introspection methods on each hierarchical level of the
+;; object path... a lot of boilerplate
+;;
+;; I think Emacs could technically generate much of this, but it doesn't seem to
+;; do so now....
+
+(defun mu4e--dbus-introspect-parents ()
+ "Set up introspection for the parent object paths."
+ (seq-do
+ (lambda (path-child)
+ (let ((path (car path-child)) (child (cdr path-child)))
+ (dbus-register-method
+ :session mu4e-dbus-service
+ path
+ dbus-interface-introspectable
+ "Introspect"
+ `(lambda ()
+ ,(format "<node><node name='%s'/></node>" child)))))
+ '(("/" . "nl")
+ ("/nl" . "djcbsoftware")
+ ("/nl/djcbsoftware" . "Mu4e"))))
+
+(defun mu4e--dbus-introspect-nl-djcbsoftware-mu4e ()
+ "Mu4e introspection for /nl/djcbsoftware/Mu4e."
+ (dbus-register-method
+ :session
+ mu4e-dbus-service
+ mu4e-dbus-object-path
+ dbus-interface-introspectable
+ "Introspect"
+ (lambda () mu4e-dbus-introspection-xml)))
+
+(defun mu4e--dbus-register-property (name access value)
+ "Register mu4e DBus property with NAME to VALUE.
+
+ACCESS is a symbol, either `:read', `:write' or `:readwrite'.
+
+There are read-only, i.e. only writable from here. Thus,
+setting a property means (re)registering it."
+ (dbus-register-property
+ :session
+ mu4e-dbus-service
+ mu4e-dbus-object-path
+ mu4e-dbus-interface
+ name access value))
+
+(defun mu4e--dbus-set-property (name value)
+ "Update mu4e DBus property with NAME to VALUE."
+ (dbus-set-property
+ :session
+ mu4e-dbus-service
+ mu4e-dbus-object-path
+ mu4e-dbus-interface
+ name value))
+
+;; Convert to D-Bus format - list of dict entries
+(defun mu4e--dbus-query-info ()
+ "Return an array of dbus vardicts with query-items information.
+
+This filters / maps the information from `mu4e-query-items'."
+ ;; D-Bus signature: aa{sv} (array of dict with string keys and variant values)
+ ;; Return an array of dbus vardicts with query-items information.
+ ;; This filters / maps the information from `mu4e-query-items'."
+ ;; D-Bus signature: aa{sv} (array of dict with string keys and variant values)
+ (cons ':array
+ (mapcar
+ (lambda (plist)
+ (let ((dict))
+ (while plist
+ (let* ((key (car plist)) (val (cadr plist)))
+ (when (memq key '(:name :query :count :unread :favorite))
+ (push (list :dict-entry (substring (symbol-name key) 1) ; eat ':'
+ (list :variant val)) dict))
+ (setq plist (cddr plist))))
+ (cons ':array dict)))
+ (mu4e-query-items))))
+
+(defun mu4e--dbus-update-query-info ()
+ "(Re-)register the query-info property."
+ (mu4e--dbus-set-property "QueryInfo" (mu4e--dbus-query-info)))
+
+(defun mu4e--dbus-update-context ()
+ "(Re-)register the current mu4e context."
+ (let ((ctx (mu4e-context-current)))
+ (mu4e--dbus-set-property
+ "Context" (if ctx (mu4e-context-name ctx) ""))))
+
+(defun mu4e--dbus-init ()
+ "Initialize mu4e's DBus interface."
+ (dbus-register-service
+ :session mu4e-dbus-service
+ :allow-replacement :replace-existing)
+
+ ;; setup introspection
+ (mu4e--dbus-introspect-parents)
+ (mu4e--dbus-introspect-nl-djcbsoftware-mu4e)
+
+ ;; Register properties
+ (mu4e--dbus-register-property "Version" :read mu4e-mu-version)
+ (mu4e--dbus-register-property "DatabasePath" :read
+ (plist-get (mu4e-server-properties) :database-path))
+ (mu4e--dbus-register-property "RootMaildir" :read
+ (plist-get (mu4e-server-properties) :root-maildir))
+ (mu4e--dbus-register-property "MessageCounbt" :readwrite
+ (plist-get (mu4e-server-properties) :doccount))
+ (mu4e--dbus-register-property "Context" :readwrite mu4e-mu-version)
+ (mu4e--dbus-register-property "QueryInfo" :readwrite (mu4e--dbus-query-info))
+
+ ;; auto-update context
+ (mu4e--dbus-update-context)
+ (add-hook 'mu4e-context-changed-hook #'mu4e--dbus-update-context)
+
+ ;; auto-update query info, and update when it changes.
+ (mu4e--dbus-update-query-info)
+ (add-hook 'mu4e-query-items-updated-hook #'mu4e--dbus-update-query-info))
+
+(defun mu4e--dbus-uninit ()
+ "Un-initialize mu4e's DBus interface."
+ (ignore-errors (dbus-unregister-service :session mu4e-dbus-service))
+ (remove-hook 'mu4e-context-changed-hook #'mu4e--dbus-update-context)
+ (remove-hook 'mu4e-query-items-updated-hook #'mu4e--dbus-update-query-info))
+
+;;;###autoload
+(define-minor-mode mu4e-dbus-mode
+ "Minor mode for enabling and disable the mu4e DBus service."
+ :global t
+ :group 'mu4e
+ :lighter nil
+ (unless (featurep 'dbus)
+ (mu4e-error "DBus support is required but not found"))
+ (if mu4e-dbus-mode
+ (mu4e--dbus-init)
+ (mu4e--dbus-uninit)))
+
+(provide 'mu4e-dbus)
+;;; mu4e-dbus.el ends here
diff --git a/mu4e/mu4e.el b/mu4e/mu4e.el
index 98562b7..1604810 100644
--- a/mu4e/mu4e.el
+++ b/mu4e/mu4e.el
@@ -49,6 +49,8 @@
(require 'mu4e-speedbar)) ;; support for speedbar
(when mu4e-org-support
(require 'mu4e-org)) ;; support for org-mode links
+(when (featurep 'dbusbind)
+ (require 'mu4e-dbus))
(defcustom mu4e-quit-hook nil
"Hook run just before quitting mu4e.
diff --git a/mu4e/mu4e.texi b/mu4e/mu4e.texi
index d6bd8d5..f8d11a5 100644
--- a/mu4e/mu4e.texi
+++ b/mu4e/mu4e.texi
@@ -3494,6 +3494,7 @@ see @ref{Other tools}.
* Modeline::Showing mu4e's status in the modeline
* Transient::Transient menus for mu4e
* Desktop notifications::Get desktop notifications for new mail
+* DBus service::Accessing Mu4e through DBus
* Emacs bookmarks::Using Emacs' bookmark system
* Eldoc::Information about the current header in the echo area
* Org-mode::Adding mu4e to your organized life
@@ -3625,6 +3626,74 @@ supported system) by setting @code{mu4e-notification-support} to @t{t}. If you
want to tweak the details, have a look at @code{mu4e-notification-filter} and
@code{mu4e-notification-function}.
+@node DBus service
+@section DBus service
+@cindex DBus
+
+DBus is a message-bus system for GNU/Linux and some other systems, which allows
+for communications between programs using a binary protocol; see @ref{(dbus)
+Top} for further details.
+
+@code{mu4e} uses Emacs' DBus-support to implements a service for making some of
+its information visible to external programs. This information includes the
+current context (@code{mu4e-context-name}) and the read/unread counts for
+various bookmarked queries (@code{mu4e-query-items})- the same information that
+is used for the new mail notifications and the read/unread counts,
+@pxref{Bookmarks and Maildirs}.
+
+An external program could use this information display such status information.
+
+@subsection @code{mu4e-dbus-mode}
+@cindex mu4e-dbus-mode
+
+Enabling and disabling the service is through a minor-mode
+@code{mu4e-dbus-mode}. @code{M-x mu4e-dbus-mode}.
+
+This starts a DBus service with the name @t{nl.djcbsoftware.nl} on the
+session-bus, and at object-path @t{nl.djcbsoftware.Mu4e}, exposes the interface
+@t{/nl/djcbsoftware/Mu4e}.
+
+The interface is described as an XML string, in a constant
+@code{mu4e-dbus-intrspection-xml}. This currently looks like:
+
+@verbatim
+ <node>
+ <interface name='org.freedesktop.DBus.Introspectable'>
+ <method name='Introspect'>
+ <arg name='xml_data' type='s' direction='out'/>
+ </method>
+ </interface>
+ <interface name='org.freedesktop.DBus.Properties'>
+ <method name='Get'>
+ <arg type='s' name='interface_name' direction='in'/>
+ <arg type='s' name='property_name' direction='in'/>
+ <arg type='v' name='value' direction='out'/>
+ </method>
+ <method name='GetAll'>
+ <arg type='s' name='interface_name' direction='in'/>
+ <arg type='a{sv}' name='properties' direction='out'/>
+ </method>
+ <method name='Set'>
+ <arg type='s' name='interface_name' direction='in'/>
+ <arg type='s' name='property_name' direction='in'/>
+ <arg type='v' name='value' direction='in'/>
+ </method>
+ <signal name='PropertiesChanged'>
+ <arg type='s' name='interface_name'/>
+ <arg type='a{sv}' name='changed_properties'/>
+ <arg type='as' name='invalidated_properties'/>
+ </signal>
+ </interface>
+ <interface name='nl.djcbsoftware.Mu4e'>
+ <property name='Version' type='s' access='read'/>
+ <property name='DatabasePath' type='s' access='read'/>
+ <property name='RootMaildir' type='s' access='read'/>
+ <property name='Context' type='s' access='readwrite'/>
+ <property name='QueryInfo' type='aa{sv}' access='readwrite'/>
+ </interface>
+</node>
+@end verbatim
+
@node Emacs bookmarks
@section Emacs bookmarks
@cindex Emacs bookmarks