1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
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
|