EasyPG 1.07 Released
[packages] / xemacs-packages / sieve / sieve-manage.el
1 ;;; sieve-manage.el --- Implementation of the managesieve protocol in elisp
2
3 ;; Copyright (C) 2001-2016 Free Software Foundation, Inc.
4
5 ;; Author: Simon Josefsson <simon@josefsson.org>
6 ;;         Albert Krewinkel <tarleb@moltkeplatz.de>
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This library provides an elisp API for the managesieve network
26 ;; protocol.
27 ;;
28 ;; It uses the SASL library for authentication, which means it
29 ;; supports DIGEST-MD5, CRAM-MD5, SCRAM-MD5, NTLM, PLAIN and LOGIN
30 ;; methods.  STARTTLS is not well tested, but should be easy to get to
31 ;; work if someone wants.
32 ;;
33 ;; The API should be fairly obvious for anyone familiar with the
34 ;; managesieve protocol, interface functions include:
35 ;;
36 ;; `sieve-manage-open'
37 ;; open connection to managesieve server, returning a buffer to be
38 ;; used by all other API functions.
39 ;;
40 ;; `sieve-manage-opened'
41 ;; check if a server is open or not
42 ;;
43 ;; `sieve-manage-close'
44 ;; close a server connection.
45 ;;
46 ;; `sieve-manage-listscripts'
47 ;; `sieve-manage-deletescript'
48 ;; `sieve-manage-getscript'
49 ;; performs managesieve protocol actions
50 ;;
51 ;; and that's it.  Example of a managesieve session in *scratch*:
52 ;;
53 ;; (with-current-buffer (sieve-manage-open "mail.example.com")
54 ;;   (sieve-manage-authenticate)
55 ;;   (sieve-manage-listscripts))
56 ;;
57 ;; => ((active . "main") "vacation")
58 ;;
59 ;; References:
60 ;;
61 ;; draft-martin-managesieve-02.txt,
62 ;; "A Protocol for Remotely Managing Sieve Scripts",
63 ;; by Tim Martin.
64 ;;
65 ;; Release history:
66 ;;
67 ;; 2001-10-31 Committed to Oort Gnus.
68 ;; 2002-07-27 Added DELETESCRIPT.  Suggested by Ned Ludd.
69 ;; 2002-08-03 Use SASL library.
70 ;; 2013-06-05 Enabled STARTTLS support, fixed bit rot.
71
72 ;;; Code:
73
74 (if (locate-library "password-cache")
75     (require 'password-cache)
76   (require 'password))
77
78 (eval-when-compile
79   (require 'cl)                         ; caddr
80   (require 'sasl)
81   (require 'starttls))
82 (autoload 'sasl-find-mechanism "sasl")
83 (autoload 'auth-source-search "auth-source")
84
85 ;; User customizable variables:
86
87 (defgroup sieve-manage nil
88   "Low-level Managesieve protocol issues."
89   :group 'mail
90   :prefix "sieve-")
91
92 (defcustom sieve-manage-log "*sieve-manage-log*"
93   "Name of buffer for managesieve session trace."
94   :type 'string
95   :group 'sieve-manage)
96
97 (defcustom sieve-manage-server-eol "\r\n"
98   "The EOL string sent from the server."
99   :type 'string
100   :group 'sieve-manage)
101
102 (defcustom sieve-manage-client-eol "\r\n"
103   "The EOL string we send to the server."
104   :type 'string
105   :group 'sieve-manage)
106
107 (defcustom sieve-manage-authenticators '(digest-md5
108                                          cram-md5
109                                          scram-md5
110                                          ntlm
111                                          plain
112                                          login)
113   "Priority of authenticators to consider when authenticating to server."
114   ;; FIXME Improve this.  It's not `set'.
115   ;; It's like (repeat (choice (const ...))), where each choice can
116   ;; only appear once.
117   :type '(repeat symbol)
118   :group 'sieve-manage)
119
120 (defcustom sieve-manage-authenticator-alist
121   '((cram-md5   sieve-manage-cram-md5-p       sieve-manage-cram-md5-auth)
122     (digest-md5 sieve-manage-digest-md5-p     sieve-manage-digest-md5-auth)
123     (scram-md5  sieve-manage-scram-md5-p      sieve-manage-scram-md5-auth)
124     (ntlm       sieve-manage-ntlm-p           sieve-manage-ntlm-auth)
125     (plain      sieve-manage-plain-p          sieve-manage-plain-auth)
126     (login      sieve-manage-login-p          sieve-manage-login-auth))
127   "Definition of authenticators.
128
129 \(NAME CHECK AUTHENTICATE)
130
131 NAME names the authenticator.  CHECK is a function returning non-nil if
132 the server support the authenticator and AUTHENTICATE is a function
133 for doing the actual authentication."
134   :type '(repeat (list (symbol :tag "Name") (function :tag "Check function")
135                        (function :tag "Authentication function")))
136   :group 'sieve-manage)
137
138 (defcustom sieve-manage-default-port "sieve"
139   "Default port number or service name for managesieve protocol."
140   :type '(choice integer string)
141   :version "24.4"
142   :group 'sieve-manage)
143
144 (defcustom sieve-manage-default-stream 'network
145   "Default stream type to use for `sieve-manage'."
146   :version "24.1"
147   :type 'symbol
148   :group 'sieve-manage)
149
150 ;; Internal variables:
151
152 (defconst sieve-manage-local-variables '(sieve-manage-server
153                                          sieve-manage-port
154                                          sieve-manage-auth
155                                          sieve-manage-stream
156                                          sieve-manage-process
157                                          sieve-manage-client-eol
158                                          sieve-manage-server-eol
159                                          sieve-manage-capability))
160 (defconst sieve-manage-coding-system-for-read 'binary)
161 (defconst sieve-manage-coding-system-for-write 'binary)
162 (defvar sieve-manage-stream nil)
163 (defvar sieve-manage-auth nil)
164 (defvar sieve-manage-server nil)
165 (defvar sieve-manage-port nil)
166 (defvar sieve-manage-state 'closed
167   "Managesieve state.
168 Valid states are `closed', `initial', `nonauth', and `auth'.")
169 (defvar sieve-manage-process nil)
170 (defvar sieve-manage-capability nil)
171
172 ;; Internal utility functions
173 (eval-and-compile
174   (if (featurep 'xemacs)
175       (defalias 'mm-enable-multibyte #'ignore)
176     (autoload 'mm-enable-multibyte "mm-util")))
177
178 (defun sieve-manage-make-process-buffer ()
179   (with-current-buffer
180       (generate-new-buffer (format " *sieve %s:%s*"
181                                    sieve-manage-server
182                                    sieve-manage-port))
183     (mapc 'make-local-variable sieve-manage-local-variables)
184     (mm-enable-multibyte)
185     (buffer-disable-undo)
186     (current-buffer)))
187
188 (defun sieve-manage-erase (&optional p buffer)
189   (let ((buffer (or buffer (current-buffer))))
190     (and sieve-manage-log
191          (with-current-buffer (get-buffer-create sieve-manage-log)
192            (mm-enable-multibyte)
193            (buffer-disable-undo)
194            (goto-char (point-max))
195            (insert-buffer-substring buffer (with-current-buffer buffer
196                                              (point-min))
197                                     (or p (with-current-buffer buffer
198                                             (point-max)))))))
199   (delete-region (point-min) (or p (point-max))))
200
201 (defun sieve-manage-open-server (server port &optional stream buffer)
202   "Open network connection to SERVER on PORT.
203 Return the buffer associated with the connection."
204   (with-current-buffer buffer
205     (sieve-manage-erase)
206     (setq sieve-manage-state 'initial)
207     (destructuring-bind (proc . props)
208         (open-protocol-stream
209          "SIEVE" buffer server port
210          :type stream
211          :capability-command "CAPABILITY\r\n"
212          :end-of-command "^\\(OK\\|NO\\).*\n"
213          :success "^OK.*\n"
214          :return-list t
215          :starttls-function
216          (lambda (capabilities)
217            (when (string-match "\\bSTARTTLS\\b" capabilities)
218              "STARTTLS\r\n")))
219       (setq sieve-manage-process proc)
220       (setq sieve-manage-capability
221             (sieve-manage-parse-capability (plist-get props :capabilities)))
222       ;; Ignore new capabilities issues after successful STARTTLS
223       (when (and (memq stream '(nil network starttls))
224                  (eq (plist-get props :type) 'tls))
225         (sieve-manage-drop-next-answer))
226       (current-buffer))))
227
228 ;; Authenticators
229 (defun sieve-sasl-auth (buffer mech)
230   "Login to server using the SASL MECH method."
231   (message "sieve: Authenticating using %s..." mech)
232   (with-current-buffer buffer
233     (let* ((auth-info (auth-source-search :host sieve-manage-server
234                                           :port "sieve"
235                                           :max 1
236                                           :create t))
237            (user-name (or (plist-get (nth 0 auth-info) :user) ""))
238            (user-password (or (plist-get (nth 0 auth-info) :secret) ""))
239            (user-password (if (functionp user-password)
240                               (funcall user-password)
241                             user-password))
242            (client (sasl-make-client (sasl-find-mechanism (list mech))
243                                      user-name "sieve" sieve-manage-server))
244            (sasl-read-passphrase
245             ;; We *need* to copy the password, because sasl will modify it
246             ;; somehow.
247             `(lambda (prompt) ,(copy-sequence user-password)))
248            (step (sasl-next-step client nil))
249            (tag (sieve-manage-send
250                  (concat
251                   "AUTHENTICATE \""
252                   mech
253                   "\""
254                   (and (sasl-step-data step)
255                        (concat
256                         " \""
257                         (base64-encode-string
258                          (sasl-step-data step)
259                          'no-line-break)
260                         "\"")))))
261            data rsp)
262       (catch 'done
263         (while t
264           (setq rsp nil)
265           (goto-char (point-min))
266           (while (null (or (progn
267                              (setq rsp (sieve-manage-is-string))
268                              (if (not (and rsp (looking-at
269                                                 sieve-manage-server-eol)))
270                                  (setq rsp nil)
271                                (goto-char (match-end 0))
272                                rsp))
273                            (setq rsp (sieve-manage-is-okno))))
274             (accept-process-output sieve-manage-process 1)
275             (goto-char (point-min)))
276           (sieve-manage-erase)
277           (when (sieve-manage-ok-p rsp)
278             (when (and (cadr rsp)
279                        (string-match "^SASL \"\\([^\"]+\\)\"" (cadr rsp)))
280               (sasl-step-set-data
281                step (base64-decode-string (match-string 1 (cadr rsp)))))
282             (if (and (setq step (sasl-next-step client step))
283                      (setq data (sasl-step-data step)))
284                 ;; We got data for server but it's finished
285                 (error "Server not ready for SASL data: %s" data)
286               ;; The authentication process is finished.
287               (throw 'done t)))
288           (unless (stringp rsp)
289             (error "Server aborted SASL authentication: %s" (caddr rsp)))
290           (sasl-step-set-data step (base64-decode-string rsp))
291           (setq step (sasl-next-step client step))
292           (sieve-manage-send
293            (if (sasl-step-data step)
294                (concat "\""
295                        (base64-encode-string (sasl-step-data step)
296                                              'no-line-break)
297                        "\"")
298              ""))))
299       (message "sieve: Login using %s...done" mech))))
300
301 (defun sieve-manage-cram-md5-p (buffer)
302   (sieve-manage-capability "SASL" "CRAM-MD5" buffer))
303
304 (defun sieve-manage-cram-md5-auth (buffer)
305   "Login to managesieve server using the CRAM-MD5 SASL method."
306   (sieve-sasl-auth buffer "CRAM-MD5"))
307
308 (defun sieve-manage-digest-md5-p (buffer)
309   (sieve-manage-capability "SASL" "DIGEST-MD5" buffer))
310
311 (defun sieve-manage-digest-md5-auth (buffer)
312   "Login to managesieve server using the DIGEST-MD5 SASL method."
313   (sieve-sasl-auth buffer "DIGEST-MD5"))
314
315 (defun sieve-manage-scram-md5-p (buffer)
316   (sieve-manage-capability "SASL" "SCRAM-MD5" buffer))
317
318 (defun sieve-manage-scram-md5-auth (buffer)
319   "Login to managesieve server using the SCRAM-MD5 SASL method."
320   (sieve-sasl-auth buffer "SCRAM-MD5"))
321
322 (defun sieve-manage-ntlm-p (buffer)
323   (sieve-manage-capability "SASL" "NTLM" buffer))
324
325 (defun sieve-manage-ntlm-auth (buffer)
326   "Login to managesieve server using the NTLM SASL method."
327   (sieve-sasl-auth buffer "NTLM"))
328
329 (defun sieve-manage-plain-p (buffer)
330   (sieve-manage-capability "SASL" "PLAIN" buffer))
331
332 (defun sieve-manage-plain-auth (buffer)
333   "Login to managesieve server using the PLAIN SASL method."
334   (sieve-sasl-auth buffer "PLAIN"))
335
336 (defun sieve-manage-login-p (buffer)
337   (sieve-manage-capability "SASL" "LOGIN" buffer))
338
339 (defun sieve-manage-login-auth (buffer)
340   "Login to managesieve server using the LOGIN SASL method."
341   (sieve-sasl-auth buffer "LOGIN"))
342
343 ;; Managesieve API
344
345 (defun sieve-manage-open (server &optional port stream auth buffer)
346   "Open a network connection to a managesieve SERVER (string).
347 Optional argument PORT is port number (integer) on remote server.
348 Optional argument STREAM is any of `sieve-manage-streams' (a symbol).
349 Optional argument AUTH indicates authenticator to use, see
350 `sieve-manage-authenticators' for available authenticators.
351 If nil, chooses the best stream the server is capable of.
352 Optional argument BUFFER is buffer (buffer, or string naming buffer)
353 to work in."
354   (setq sieve-manage-port (or port sieve-manage-default-port))
355   (with-current-buffer (or buffer (sieve-manage-make-process-buffer))
356     (setq sieve-manage-server (or server
357                                   sieve-manage-server)
358           sieve-manage-stream (or stream
359                                   sieve-manage-stream
360                                   sieve-manage-default-stream)
361           sieve-manage-auth   (or auth
362                                   sieve-manage-auth))
363     (message "sieve: Connecting to %s..." sieve-manage-server)
364     (sieve-manage-open-server sieve-manage-server
365                               sieve-manage-port
366                               sieve-manage-stream
367                               (current-buffer))
368     (when (sieve-manage-opened (current-buffer))
369       ;; Choose authenticator
370       (when (and (null sieve-manage-auth)
371                  (not (eq sieve-manage-state 'auth)))
372         (dolist (auth sieve-manage-authenticators)
373           (when (funcall (nth 1 (assq auth sieve-manage-authenticator-alist))
374                        buffer)
375             (setq sieve-manage-auth auth)
376             (return)))
377         (unless sieve-manage-auth
378           (error "Couldn't figure out authenticator for server")))
379       (sieve-manage-erase)
380       (current-buffer))))
381
382 (defun sieve-manage-authenticate (&optional buffer)
383   "Authenticate on server in BUFFER.
384 Return `sieve-manage-state' value."
385   (with-current-buffer (or buffer (current-buffer))
386     (if (eq sieve-manage-state 'nonauth)
387         (when (funcall (nth 2 (assq sieve-manage-auth
388                                     sieve-manage-authenticator-alist))
389                        (current-buffer))
390           (setq sieve-manage-state 'auth))
391       sieve-manage-state)))
392
393 (defun sieve-manage-opened (&optional buffer)
394   "Return non-nil if connection to managesieve server in BUFFER is open.
395 If BUFFER is nil then the current buffer is used."
396   (and (setq buffer (get-buffer (or buffer (current-buffer))))
397        (buffer-live-p buffer)
398        (with-current-buffer buffer
399          (and sieve-manage-process
400               (memq (process-status sieve-manage-process) '(open run))))))
401
402 (defun sieve-manage-close (&optional buffer)
403   "Close connection to managesieve server in BUFFER.
404 If BUFFER is nil, the current buffer is used."
405   (with-current-buffer (or buffer (current-buffer))
406     (when (sieve-manage-opened)
407       (sieve-manage-send "LOGOUT")
408       (sit-for 1))
409     (when (and sieve-manage-process
410                (memq (process-status sieve-manage-process) '(open run)))
411       (delete-process sieve-manage-process))
412     (setq sieve-manage-process nil)
413     (sieve-manage-erase)
414     t))
415
416 (defun sieve-manage-capability (&optional name value buffer)
417   "Check if capability NAME of server BUFFER match VALUE.
418 If it does, return the server value of NAME. If not returns nil.
419 If VALUE is nil, do not check VALUE and return server value.
420 If NAME is nil, return the full server list of capabilities."
421   (with-current-buffer (or buffer (current-buffer))
422     (if (null name)
423         sieve-manage-capability
424       (let ((server-value (cadr (assoc name sieve-manage-capability))))
425         (when (or (null value)
426                   (and server-value
427                        (string-match value server-value)))
428           server-value)))))
429
430 (defun sieve-manage-listscripts (&optional buffer)
431   (with-current-buffer (or buffer (current-buffer))
432     (sieve-manage-send "LISTSCRIPTS")
433     (sieve-manage-parse-listscripts)))
434
435 (defun sieve-manage-havespace (name size &optional buffer)
436   (with-current-buffer (or buffer (current-buffer))
437     (sieve-manage-send (format "HAVESPACE \"%s\" %s" name size))
438     (sieve-manage-parse-okno)))
439
440 (defun sieve-manage-putscript (name content &optional buffer)
441   (with-current-buffer (or buffer (current-buffer))
442     (sieve-manage-send (format "PUTSCRIPT \"%s\" {%d+}%s%s" name
443                                ;; Here we assume that the coding-system will
444                                ;; replace each char with a single byte.
445                                ;; This is always the case if `content' is
446                                ;; a unibyte string.
447                                (length content)
448                                sieve-manage-client-eol content))
449     (sieve-manage-parse-okno)))
450
451 (defun sieve-manage-deletescript (name &optional buffer)
452   (with-current-buffer (or buffer (current-buffer))
453     (sieve-manage-send (format "DELETESCRIPT \"%s\"" name))
454     (sieve-manage-parse-okno)))
455
456 (defun sieve-manage-getscript (name output-buffer &optional buffer)
457   (with-current-buffer (or buffer (current-buffer))
458     (sieve-manage-send (format "GETSCRIPT \"%s\"" name))
459     (let ((script (sieve-manage-parse-string)))
460       (sieve-manage-parse-crlf)
461       (with-current-buffer output-buffer
462         (insert script))
463       (sieve-manage-parse-okno))))
464
465 (defun sieve-manage-setactive (name &optional buffer)
466   (with-current-buffer (or buffer (current-buffer))
467     (sieve-manage-send (format "SETACTIVE \"%s\"" name))
468     (sieve-manage-parse-okno)))
469
470 ;; Protocol parsing routines
471
472 (defun sieve-manage-wait-for-answer ()
473   (let ((pattern "^\\(OK\\|NO\\).*\n")
474         pos)
475     (while (not pos)
476       (setq pos (search-forward-regexp pattern nil t))
477       (goto-char (point-min))
478       (sleep-for 0 50))
479     pos))
480
481 (defun sieve-manage-drop-next-answer ()
482   (sieve-manage-wait-for-answer)
483   (sieve-manage-erase))
484
485 (defun sieve-manage-ok-p (rsp)
486   (string= (downcase (or (car-safe rsp) "")) "ok"))
487
488 (defun sieve-manage-is-okno ()
489   (when (looking-at (concat
490                      "^\\(OK\\|NO\\)\\( (\\([^)]+\\))\\)?\\( \\(.*\\)\\)?"
491                      sieve-manage-server-eol))
492     (let ((status (match-string 1))
493           (resp-code (match-string 3))
494           (response (match-string 5)))
495       (when response
496         (goto-char (match-beginning 5))
497         (setq response (sieve-manage-is-string)))
498       (list status resp-code response))))
499
500 (defun sieve-manage-parse-okno ()
501   (let (rsp)
502     (while (null rsp)
503       (accept-process-output (get-buffer-process (current-buffer)) 1)
504       (goto-char (point-min))
505       (setq rsp (sieve-manage-is-okno)))
506     (sieve-manage-erase)
507     rsp))
508
509 ;; SXEmacs change: we don't have #'split-string-and-unquote, but it
510 ;; does pretty much the exact same thing as #'split-string so using
511 ;; that for now as it should be good enough. --SY.
512 (defun sieve-manage-parse-capability (str)
513   "Parse managesieve capability string `STR'.
514 Set variable `sieve-manage-capability' to "
515   (let ((capas (delq nil
516                      (mapcar #'split-string
517                              (split-string str "\n")))))
518     (when (string= "OK" (caar (last capas)))
519       (setq sieve-manage-state 'nonauth))
520     capas))
521
522 (defun sieve-manage-is-string ()
523   (cond ((looking-at "\"\\([^\"]+\\)\"")
524          (prog1
525              (match-string 1)
526            (goto-char (match-end 0))))
527         ((looking-at (concat "{\\([0-9]+\\+?\\)}" sieve-manage-server-eol))
528          (let ((pos (match-end 0))
529                (len (string-to-number (match-string 1))))
530            (if (< (point-max) (+ pos len))
531                nil
532              (goto-char (+ pos len))
533              (buffer-substring pos (+ pos len)))))))
534
535 (defun sieve-manage-parse-string ()
536   (let (rsp)
537     (while (null rsp)
538       (accept-process-output (get-buffer-process (current-buffer)) 1)
539       (goto-char (point-min))
540       (setq rsp (sieve-manage-is-string)))
541     (sieve-manage-erase (point))
542     rsp))
543
544 (defun sieve-manage-parse-crlf ()
545   (when (looking-at sieve-manage-server-eol)
546     (sieve-manage-erase (match-end 0))))
547
548 (defun sieve-manage-parse-listscripts ()
549   (let (tmp rsp data)
550     (while (null rsp)
551       (while (null (or (setq rsp (sieve-manage-is-okno))
552                        (setq tmp (sieve-manage-is-string))))
553         (accept-process-output (get-buffer-process (current-buffer)) 1)
554         (goto-char (point-min)))
555       (when tmp
556         (while (not (looking-at (concat "\\( ACTIVE\\)?"
557                                         sieve-manage-server-eol)))
558           (accept-process-output (get-buffer-process (current-buffer)) 1)
559           (goto-char (point-min)))
560         (if (match-string 1)
561             (push (cons 'active tmp) data)
562           (push tmp data))
563         (goto-char (match-end 0))
564         (setq tmp nil)))
565     (sieve-manage-erase)
566     (if (sieve-manage-ok-p rsp)
567         data
568       rsp)))
569
570 (defun sieve-manage-send (cmdstr)
571   (setq cmdstr (concat cmdstr sieve-manage-client-eol))
572   (and sieve-manage-log
573        (with-current-buffer (get-buffer-create sieve-manage-log)
574          (mm-enable-multibyte)
575          (buffer-disable-undo)
576          (goto-char (point-max))
577          (insert cmdstr)))
578   (process-send-string sieve-manage-process cmdstr))
579
580 (provide 'sieve-manage)
581
582 ;; sieve-manage.el ends here