]> code.delx.au - gnu-emacs-elpa/blob - packages/excorporate/excorporate.el
packages/excorporate: Change server version lookup
[gnu-emacs-elpa] / packages / excorporate / excorporate.el
1 ;;; excorporate.el --- Exchange integration -*- lexical-binding: t -*-
2
3 ;; Copyright (C) 2014-2016 Free Software Foundation, Inc.
4
5 ;; Author: Thomas Fitzsimmons <fitzsim@fitzsim.org>
6 ;; Maintainer: Thomas Fitzsimmons <fitzsim@fitzsim.org>
7 ;; Created: 2014-09-19
8 ;; Version: 0.7.1
9 ;; Keywords: calendar
10 ;; Homepage: https://www.fitzsim.org/blog/
11 ;; Package-Requires: ((emacs "24.1") (fsm "0.2") (soap-client "3.0.2") (url-http-ntlm "2.0.2"))
12
13 ;; This program is free software: you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
17
18 ;; This program is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
22
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 ;;; Commentary:
27
28 ;; Excorporate provides Exchange integration for Emacs.
29
30 ;; To create a connection to a web service:
31
32 ;; M-x excorporate
33
34 ;; Excorporate will prompt for an email address that it will use to
35 ;; automatically discover settings. Then it will connect to two or
36 ;; three separate hosts: the autodiscovery host, the web service host
37 ;; or load balancer, and the actual server if there is a load
38 ;; balancer. Therefore you may be prompted for your credentials two
39 ;; or three times.
40
41 ;; You should see a message indicating that the connection is ready
42 ;; either in the minibuffer or failing that in the *Messages* buffer.
43
44 ;; Finally, run M-x calendar, and press 'e' to show today's meetings.
45
46 ;; Please try autodiscovery first and report issues not yet listed
47 ;; below. When autodiscovery works it is very convenient; the goal is
48 ;; to make it work for as many users as possible.
49
50 ;; If autodiscovery fails, customize `excorporate-configuration' to
51 ;; skip autodiscovery.
52
53 ;; Autodiscovery will fail if:
54
55 ;; - Excorporate is accessing the server through a proxy (Emacs
56 ;; bug#10).
57
58 ;; - The server is not configured to support autodiscovery.
59
60 ;; - The email address is at a different domain than the server, e.g.,
61 ;; user@domain1.com, autodiscover.domain2.com.
62
63 ;; - Authentication is Kerberos/GSSAPI.
64
65 ;; Excorporate does know about the special case where the mail address
66 ;; is at a subdomain, e.g., user@sub.domain.com, and the server is at
67 ;; the main domain, e.g., autodiscover.domain.com. Autodiscovery will
68 ;; work in that case.
69
70 ;; Excorporate must be loaded before any other package that requires
71 ;; `soap-client'. The version of `soap-client' that Excorporate
72 ;; bundles is backward compatible.
73
74 ;; Acknowledgments:
75
76 ;; Alexandru Harsanyi <AlexHarsanyi@gmail.com> provided help and
77 ;; guidance on how to extend soap-client.el's WSDL and XSD handling,
78 ;; enabling support for the full Exchange Web Services API.
79
80 ;; Alex Luccisano <casual.lexicon@gmail.com> tested early versions of
81 ;; this library against a corporate installation of Exchange.
82
83 ;; Jon Miller <jonebird@gmail.com> tested against Exchange 2013. He
84 ;; also tracked down and reported a bad interaction with other
85 ;; packages that require soap-client.
86
87 ;; Nicolas Lamirault <nicolas.lamirault@gmail.com> tested the
88 ;; autodiscovery feature.
89
90 ;; Trey Jackson <bigfaceworm@gmail.com> confirmed autodiscovery worked
91 ;; for him.
92
93 ;; Joakim Verona <joakim@verona.se> tested autodiscovery in a
94 ;; Kerberos/GSSAPI environment.
95
96 ;; Wilfred Hughes <me@wilfred.me.uk> tested on Exchange 2007 and
97 ;; suggested documentation improvements.
98
99 ;;; Code:
100 \f
101 ;; Implementation-visible functions and variables.
102
103 ;; Add NTLM authorization scheme.
104 (require 'url-http-ntlm)
105 (require 'soap-client)
106 (require 'fsm)
107 (require 'excorporate-calendar)
108
109 (defconst exco--autodiscovery-templates
110 '("https://%s/autodiscover/autodiscover.svc"
111 "https://autodiscover.%s/autodiscover/autodiscover.svc")
112 "Autodiscovery URL templates.
113 URL templates to be formatted with a domain name, then searched
114 for autodiscovery files.")
115
116 (defvar exco--connections nil
117 "A hash table of finite state machines.
118 The key is the identifier passed to `exco-connect'. Each finite
119 state machine represents a service connection.")
120
121 (defvar exco--connection-identifiers nil
122 "An ordered list of connection identifiers.")
123
124 (defun exco--parse-xml-in-current-buffer ()
125 "Decode and parse the XML contents of the current buffer."
126 (let ((mime-part (mm-dissect-buffer t t)))
127 (unless mime-part
128 (error "Failed to decode response from server"))
129 (unless (equal (car (mm-handle-type mime-part)) "text/xml")
130 (error "Server response is not an XML document"))
131 (with-temp-buffer
132 (mm-insert-part mime-part)
133 (prog1
134 (car (xml-parse-region (point-min) (point-max)))
135 (kill-buffer)
136 (mm-destroy-part mime-part)))))
137
138 (defun exco--bind-wsdl (wsdl service-url port-name target-namespace
139 binding-name)
140 "Create a WSDL binding.
141 Create a binding port for WSDL from SERVICE-URL, PORT-NAME,
142 TARGET-NAMESPACE and BINDING-NAME."
143 (let* ((namespace (soap-wsdl-find-namespace target-namespace wsdl))
144 (port (make-soap-port
145 :name port-name
146 :binding (cons target-namespace binding-name)
147 :service-url service-url)))
148 (soap-namespace-put port namespace)
149 (push port (soap-wsdl-ports wsdl))
150 (soap-resolve-references port wsdl)
151 wsdl))
152
153 (defun exco--handle-url-error (url status)
154 "Handle an error that occurred when retrieving URL.
155 The details of the error are in STATUS, in the same format as the
156 argument to a `url-retrieve' callback. Return non-nil to retry,
157 nil to continue."
158 (if (eq (cl-third (plist-get status :error)) 500)
159 ;; The server reported an internal server error. Try to recover
160 ;; by re-requesting the target URL and its most recent redirect.
161 ;; I'm not sure what conditions cause the server to get into
162 ;; this state -- it might be because the server has stale
163 ;; knowledge of old keepalive connections -- but this should
164 ;; recover it. We need to disable ntlm in
165 ;; url-registered-auth-schemes so that it doesn't prevent
166 ;; setting keepalives to nil.
167 (let ((url-registered-auth-schemes nil)
168 (url-http-attempt-keepalives nil)
169 (redirect (plist-get status :redirect)))
170 (fsm-debug-output "exco--fsm received 500 error for %s" url)
171 (url-debug 'excorporate "Attempting 500 recovery")
172 (ignore-errors
173 ;; Emacs's url-retrieve does not respect the values of
174 ;; url-http-attempt-keepalives and
175 ;; url-registered-auth-schemes in asynchronous contexts.
176 ;; Unless url.el is eventually changed to do so, the
177 ;; following requests must be synchronous so that they run
178 ;; entirely within url-http-attempt-keepalives's dynamic
179 ;; extent. These calls block the main event loop,
180 ;; unfortunately, but only in this rare error recovery
181 ;; scenario.
182 (url-retrieve-synchronously url)
183 (when redirect (url-retrieve-synchronously redirect)))
184 (url-debug 'excorporate "Done 500 recovery attempt")
185 ;; Retry.
186 t)
187 ;; We received some other error, which just
188 ;; means we should try the next URL.
189 (fsm-debug-output "exco--fsm didn't find %s" url)
190 ;; Don't retry.
191 nil))
192
193 (defun exco--retrieve-next-import (fsm state-data return-for next-state)
194 "Retrieve the next XML schema import.
195 FSM is the finite state machine, STATE-DATA is FSM's state data,
196 and RETURN-FOR is one of :enter or :event to indicate what return
197 type the calling function expects. NEXT-STATE is the next state
198 the FSM should transition to on success."
199 (let* ((url (plist-get state-data :service-url))
200 (xml (plist-get state-data :service-xml))
201 (wsdl (plist-get state-data :service-wsdl))
202 (imports (soap-wsdl-xmlschema-imports wsdl))
203 (next-state (if imports :parsing-service-wsdl next-state)))
204 (when imports
205 (let ((import-url (url-expand-file-name (pop imports) url)))
206 (let ((url-request-method "GET")
207 (url-package-name "soap-client.el")
208 (url-package-version "1.0")
209 (url-mime-charset-string "utf-8;q=1, iso-8859-1;q=0.5")
210 (url-http-attempt-keepalives t))
211 (url-retrieve
212 import-url
213 (lambda (status)
214 (let ((data-buffer (current-buffer)))
215 (unwind-protect
216 (progn
217 (url-debug 'excorporate "Processing import %s" status)
218 (if (eq (car status) :error)
219 ;; There is an error. It may be recoverable
220 ;; if it's HTTP 500 (internal server error).
221 (if (and (exco--handle-url-error import-url status)
222 ;; Only retry once.
223 (not (plist-get state-data :retrying)))
224 ;; We should retry. Don't save the
225 ;; popped urls list to state-data, so
226 ;; that this :try-next-url will
227 ;; re-attempt to retrieve the same car as
228 ;; before. Set the retry flag.
229 (progn
230 (plist-put state-data :retrying t))
231 ;; Save the popped urls list so that the next url
232 ;; is attempted, and clear the retry flag.
233 (plist-put state-data :retrying nil)
234 (setf (soap-wsdl-xmlschema-imports wsdl) imports)
235 (plist-put state-data :failure-message
236 (format "Failed to retrieve %s"
237 import-url))
238 (fsm-send fsm :unrecoverable-error))
239 ;; Success, parse WSDL.
240 (plist-put state-data :retrying nil)
241 (setf (soap-wsdl-xmlschema-imports wsdl) imports)
242 (soap-with-local-xmlns xml
243 (soap-wsdl-add-namespace
244 (soap-parse-schema (soap-parse-server-response) wsdl)
245 wsdl))
246 (plist-put state-data :service-wsdl wsdl)))
247 (and (buffer-live-p data-buffer)
248 (kill-buffer data-buffer))))
249 (fsm-send fsm t))))))
250 (if (eq return-for :enter)
251 (list state-data nil)
252 (list next-state state-data nil))))
253
254 (define-state-machine exco--fsm :start
255 ((identifier)
256 "Start an Excorporate finite state machine."
257 (if (stringp identifier)
258 (let ((domain (cadr (split-string identifier "@"))))
259 (unless (and domain (not (equal domain "")))
260 (error "Invalid domain for address %s" identifier))
261 (list :retrieving-autodiscovery-xml
262 (list
263 ;; State machine data.
264 ;; Unique finite state machine identifier. Either mail-address
265 ;; or (mail-address . service-url). The latter allows multiple
266 ;; state machines to operate on the same service URL. Login
267 ;; credentials are handled separately by auth-source and url,
268 ;; so these should be the only two identifier types needed here.
269 :identifier identifier
270 ;; User data.
271 :mail-address identifier
272 ;; Error recovery data.
273 :retrying nil
274 ;; Autodiscovery data.
275 :autodiscovery-urls
276 (append (mapcar (lambda (template)
277 (format template domain))
278 exco--autodiscovery-templates)
279 ;; Handle the user@sub.domain.com =>
280 ;; autodiscover.domain.com case reported by a
281 ;; user. Only try one extra level.
282 (let ((domain-parts (split-string domain "\\.")))
283 (when (> (length domain-parts) 2)
284 (mapcar (lambda (template)
285 (format template
286 (mapconcat
287 'identity
288 (cdr domain-parts) ".")))
289 exco--autodiscovery-templates))))
290 ;; Service data.
291 :service-url nil
292 :service-xml nil
293 :service-wsdl nil
294 ;; State data.
295 :next-state-after-success nil
296 :failure-message nil
297 :server-version nil)
298 ;; No timeout.
299 nil))
300 ;; Go directly to :retrieving-service-xml, skipping autodiscovery.
301 (list :retrieving-service-xml
302 (list
303 :identifier identifier
304 :mail-address (car identifier)
305 :retrying nil
306 :autodiscovery-urls nil
307 ;; Use service-url field from identifier.
308 :service-url (cdr identifier)
309 :service-xml nil
310 :service-wsdl nil
311 :next-state-after-success nil
312 :failure-message nil
313 :server-version nil)
314 ;; No timeout.
315 nil))))
316
317 (define-state exco--fsm :retrieving-autodiscovery-xml
318 (fsm state-data event _callback)
319 (cl-case event
320 (:try-next-url
321 (let ((urls (plist-get state-data :autodiscovery-urls)))
322 (if urls
323 (let ((url (pop urls)))
324 (fsm-debug-output "exco--fsm will probe %s" url)
325 (condition-case nil
326 (url-retrieve
327 url
328 (lambda (status)
329 (let ((data-buffer (current-buffer)))
330 (unwind-protect
331 (progn
332 (url-debug 'excorporate
333 "Processing status: %s" status)
334 (if (eq (car status) :error)
335 (progn
336 (if (and
337 (exco--handle-url-error url status)
338 ;; Only retry once.
339 (not (plist-get state-data :retrying)))
340 ;; We should retry. Don't save the popped
341 ;; urls list to state-data, so that this
342 ;; :try-next-url will re-attempt to
343 ;; retrieve the same car as before. Set
344 ;; the retry flag.
345 (plist-put state-data :retrying t)
346 ;; Save the popped urls list so that the
347 ;; next url is attempted, and clear the
348 ;; retry flag.
349 (plist-put state-data :retrying nil)
350 (plist-put state-data
351 :autodiscovery-urls urls))
352 ;; Try next or retry.
353 (fsm-send fsm :try-next-url))
354 ;; Success, save URL and parse returned XML.
355 (message
356 "Excorporate: Found autodiscovery URL for %S: %s"
357 (plist-get state-data :identifier) url)
358 (plist-put state-data :retrying nil)
359 (plist-put state-data :service-url url)
360 (plist-put state-data :service-xml
361 (exco--parse-xml-in-current-buffer))
362 (fsm-send fsm :success))
363 (url-debug 'excorporate "Done processing status"))
364 (and (buffer-live-p data-buffer)
365 (kill-buffer data-buffer))))))
366 (error
367 (fsm-debug-output "exco--fsm connection refused for %s" url)
368 (plist-put state-data :retrying nil)
369 (plist-put state-data :autodiscovery-urls urls)
370 (fsm-send fsm :try-next-url)))
371 (list :retrieving-autodiscovery-xml state-data nil))
372 (plist-put state-data :failure-message
373 "Autodiscovery ran out of URLs to try")
374 (list :shutting-down-on-error state-data nil))))
375 (:success
376 (plist-put state-data :next-state-after-success :retrieving-service-xml)
377 (list :parsing-service-wsdl state-data nil))))
378
379 (define-enter-state exco--fsm :shutting-down-on-error
380 (_fsm state-data)
381 (let ((failure-message (plist-get state-data :failure-message)))
382 (exco-disconnect (plist-get state-data :identifier))
383 (message "Excorporate: %s" failure-message)
384 (url-debug 'excorporate "Failed: %s" failure-message)
385 (fsm-debug-output "exco--fsm failed: %s" failure-message))
386 (list state-data nil))
387
388 (define-state exco--fsm :shutting-down-on-error
389 (_fsm state-data _event _callback)
390 (list :shutting-down-on-error state-data nil))
391
392 (define-enter-state exco--fsm :retrieving-service-xml
393 (fsm state-data)
394 (when (stringp (plist-get state-data :identifier))
395 (let* ((xml (plist-get state-data :service-xml))
396 (unbound-wsdl (plist-get state-data :service-wsdl))
397 (wsdl
398 (progn
399 ;; Skip soap-parse-wsdl-phase-fetch-schema to avoid
400 ;; synchronous URL fetches.
401 (soap-parse-wsdl-phase-finish-parsing xml unbound-wsdl)
402 (exco--bind-wsdl
403 (soap-wsdl-resolve-references unbound-wsdl)
404 (plist-get state-data :service-url)
405 "AutodiscoverServicePort"
406 "http://schemas.microsoft.com/exchange/2010/Autodiscover"
407 "DefaultBinding_Autodiscover"))))
408 (soap-invoke-async
409 (lambda (response)
410 (let ((result-url
411 (exco-extract-value '(Response
412 UserResponses
413 UserResponse
414 UserSettings
415 UserSetting
416 Value)
417 response)))
418 (if result-url
419 (progn
420 (plist-put state-data :service-url result-url)
421 (message "Excorporate: Found service URL for %S: %s"
422 (plist-get state-data :identifier)
423 (plist-get state-data :service-url)))
424 ;; No result. Check for error.
425 (let ((error-message
426 (exco-extract-value '(Response
427 UserResponses
428 UserResponse
429 ErrorMessage)
430 response)))
431 (if error-message
432 (message "Excorporate: %s" error-message)
433 (message "Excorporate: Failed to find service URL"))))
434 (fsm-send fsm :retrieve-xml)))
435 nil
436 wsdl
437 "AutodiscoverServicePort"
438 "GetUserSettings"
439 `((RequestedServerVersion . "Exchange2010")
440 (Request
441 (Users
442 (User
443 (Mailbox . ,(plist-get state-data :mail-address))))
444 (RequestedSettings
445 (Setting . "InternalEwsUrl")))))))
446 (list state-data nil))
447
448 (define-state exco--fsm :retrieving-service-xml
449 (fsm state-data event _callback)
450 (cl-case event
451 (:unrecoverable-error
452 (list :shutting-down-on-error state-data nil))
453 (:retrieve-xml
454 (let* ((service-url (plist-get state-data :service-url))
455 (wsdl-url (replace-regexp-in-string "/[^/]*$" "/Services.wsdl"
456 service-url)))
457 (url-retrieve wsdl-url
458 (lambda (status)
459 (let ((data-buffer (current-buffer)))
460 (unwind-protect
461 (if (eq (car status) :error)
462 (progn
463 (plist-put state-data :failure-message
464 (format "Failed to retrieve %s"
465 wsdl-url))
466 (fsm-send fsm :unrecoverable-error))
467 (plist-put state-data
468 :service-xml
469 (exco--parse-xml-in-current-buffer))
470 (fsm-send fsm :success))
471 (and (buffer-live-p data-buffer)
472 (kill-buffer data-buffer)))))))
473 (list :retrieving-service-xml state-data nil))
474 (:success
475 (plist-put state-data :next-state-after-success :retrieving-data)
476 (list :parsing-service-wsdl state-data nil))))
477
478 (define-enter-state exco--fsm :parsing-service-wsdl
479 (fsm state-data)
480 (let* ((url (plist-get state-data :service-url))
481 (xml (plist-get state-data :service-xml))
482 (next-state (plist-get state-data :next-state-after-success))
483 (wsdl (soap-make-wsdl url)))
484 (soap-parse-wsdl-phase-validate-node xml)
485 ;; Skip soap-parse-wsdl-phase-fetch-imports to avoid synchronous
486 ;; fetches of import URLs.
487 (soap-parse-wsdl-phase-parse-schema xml wsdl)
488 (plist-put state-data :service-wsdl wsdl)
489 (exco--retrieve-next-import fsm state-data :enter next-state)))
490
491 (define-state exco--fsm :parsing-service-wsdl
492 (fsm state-data event _callback)
493 (if (eq event :unrecoverable-error)
494 (list :shutting-down-on-error state-data nil)
495 (let ((next-state (plist-get state-data :next-state-after-success)))
496 (exco--retrieve-next-import fsm state-data :event next-state))))
497
498 (defun exco--get-server-version (wsdl)
499 "Extract server version from WSDL."
500 (let ((warning-message "Excorporate: Failed to determine server version")
501 (namespace "http://schemas.microsoft.com/exchange/services/2006/types")
502 (name "RequestServerVersion")
503 (found-version nil))
504 (unwind-protect
505 (setq found-version
506 (catch 'found
507 (dolist (attribute
508 (soap-xs-type-attributes
509 (soap-xs-element-type (soap-wsdl-get
510 `(,namespace . ,name)
511 wsdl 'soap-xs-element-p))))
512 (when (equal (soap-xs-attribute-name attribute) "Version")
513 (throw 'found (car (soap-xs-simple-type-enumeration
514 (soap-xs-attribute-type attribute))))))
515 (warn warning-message)
516 nil))
517 (if found-version
518 found-version
519 (warn warning-message)
520 nil))))
521
522 (define-enter-state exco--fsm :retrieving-data
523 (_fsm state-data)
524 (let ((wsdl (plist-get state-data :service-wsdl))
525 (identifier (plist-get state-data :identifier)))
526 ;; Skip soap-parse-wsdl-phase-fetch-schema to avoid synchronous
527 ;; URL fetches.
528 (soap-parse-wsdl-phase-finish-parsing (plist-get state-data :service-xml)
529 wsdl)
530 (exco--bind-wsdl
531 (soap-wsdl-resolve-references wsdl)
532 (plist-get state-data :service-url)
533 "ExchangeServicePort"
534 "http://schemas.microsoft.com/exchange/services/2006/messages"
535 "ExchangeServiceBinding")
536 (plist-put state-data :server-version (exco--get-server-version wsdl))
537 (fsm-debug-output "exco--fsm %s server version is %s"
538 identifier (exco-server-version identifier))
539 (message "Excorporate: Connection %S is ready" identifier))
540 (list state-data nil))
541
542 (define-state exco--fsm :retrieving-data
543 (_fsm state-data event _callback)
544 (let* ((identifier (plist-get state-data :identifier))
545 (wsdl (plist-get state-data :service-wsdl))
546 (name (pop event))
547 (arguments (pop event))
548 (callback (pop event)))
549 (apply #'soap-invoke-async
550 (lambda (response)
551 (funcall callback identifier response))
552 nil
553 wsdl
554 "ExchangeServicePort"
555 name
556 arguments))
557 (list :retrieving-data state-data nil))
558
559 (defun exco--ensure-connection ()
560 "Ensure at least one connection exists or throw an error."
561 (unless exco--connection-identifiers
562 (error "Excorporate: No connections exist. Run M-x excorporate")))
563
564 (defmacro exco--with-fsm (identifier &rest body)
565 "With `fsm' set to IDENTIFIER, run BODY.
566 Run BODY with `fsm' set to the finite state machine specified by
567 IDENTIFIER."
568 (declare (indent 1) (debug t))
569 `(progn
570 (exco--ensure-connection)
571 (let ((fsm (gethash ,identifier exco--connections)))
572 (unless fsm
573 (error "Excorporate: Connection %S does not exist" ,identifier))
574 ,@body)))
575 \f
576 ;; Developer-visible functions and variables.
577
578 (defun exco-api-version ()
579 "Return the Excorporate API version.
580 Return a non-negative integer representing the current
581 Excorporate application programming interface version. Version 0
582 is subject to change."
583 0)
584
585 (defun exco-connect (identifier)
586 "Connect or reconnect to a web service.
587 IDENTIFIER is the mail address to use for autodiscovery or a
588 pair (mail-address . service-url)."
589 (if (stringp identifier)
590 (message "Excorporate: Starting autodiscovery for %S"
591 identifier))
592 (let ((fsm (start-exco--fsm identifier)))
593 (unless exco--connections
594 (setq exco--connections (make-hash-table :test 'equal)))
595 (when (gethash identifier exco--connections)
596 (exco-disconnect identifier))
597 (puthash identifier fsm exco--connections)
598 (push identifier exco--connection-identifiers)
599 (if (stringp identifier)
600 (fsm-send fsm :try-next-url)
601 (fsm-send fsm :retrieve-xml))
602 nil))
603
604 (defun exco-operate (identifier name arguments callback)
605 "Execute a service operation asynchronously.
606 IDENTIFIER is the connection identifier. Execute operation NAME
607 with ARGUMENTS then call CALLBACK with two arguments, IDENTIFIER
608 and the server's response."
609 (exco--with-fsm identifier
610 (fsm-send fsm (list name arguments callback)))
611 nil)
612
613 (defun exco-server-version (identifier)
614 "Return the server version for connection IDENTIFIER, as a string.
615 Examples are \"Exchange2010\", \"Exchange2010_SP1\",
616 \"Exchange2013\"."
617 (exco--with-fsm identifier
618 (plist-get (fsm-get-state-data fsm) :server-version)))
619
620 (defun exco-disconnect (identifier)
621 "Disconnect from a web service.
622 IDENTIFIER is the mail address used to look up the connection."
623 (exco--with-fsm identifier
624 (setq exco--connection-identifiers
625 (delete identifier exco--connection-identifiers))
626 (remhash identifier exco--connections))
627 nil)
628
629 (defun exco-extract-value (path result)
630 "Extract the value at PATH from RESULT.
631 PATH is an ordered list of node names."
632 (let ((values (nreverse (car result))))
633 (dolist (path-element path)
634 (setq values (assoc path-element values)))
635 (cdr values)))
636
637 (defun exco-calendar-item-iterate (response callback)
638 "Iterate through calendar items in RESPONSE, calling CALLBACK on each.
639 Returns a list of results from callback. CALLBACK takes arguments:
640 SUBJECT, a string, the subject of the meeting.
641 START, the start date and time in Emacs internal representation.
642 END, the start date and time in Emacs internal representation.
643 LOCATION, the location of the meeting.
644 MAIN-INVITEES, a list of strings representing required participants.
645 OPTIONAL-INVITEES, a list of strings representing optional participants."
646 (let ((result-list '()))
647 (dolist (calendar-item (exco-extract-value '(ResponseMessages
648 FindItemResponseMessage
649 RootFolder
650 Items)
651 response))
652 (let* ((subject (cdr (assoc 'Subject calendar-item)))
653 (start (cdr (assoc 'Start calendar-item)))
654 (start-internal (apply #'encode-time
655 (soap-decode-date-time
656 start 'dateTime)))
657 (end (cdr (assoc 'End calendar-item)))
658 (end-internal (apply #'encode-time
659 (soap-decode-date-time
660 end 'dateTime)))
661 (location (cdr (assoc 'Location calendar-item)))
662 (to-invitees (cdr (assoc 'DisplayTo calendar-item)))
663 (main-invitees (when to-invitees
664 (mapcar 'org-trim
665 (split-string to-invitees ";"))))
666 (cc-invitees (cdr (assoc 'DisplayCc calendar-item)))
667 (optional-invitees (when cc-invitees
668 (mapcar 'org-trim
669 (split-string cc-invitees ";")))))
670 (push (funcall callback subject start-internal end-internal
671 location main-invitees optional-invitees)
672 result-list)))
673 (nreverse result-list)))
674
675 ;; Date-time utility functions.
676 (defun exco-extend-timezone (date-time-string)
677 "Add a colon to the timezone in DATE-TIME-STRING.
678 DATE-TIME-STRING must be formatted as if returned by
679 `format-time-string' with FORMAT-STRING \"%FT%T%z\". Web
680 services require the ISO8601 extended format of timezone, which
681 includes the colon."
682 (concat
683 (substring date-time-string 0 22) ":" (substring date-time-string 22)))
684
685 (defun exco-format-date-time (time-internal)
686 "Convert TIME-INTERNAL to an XSD compatible date-time string."
687 (exco-extend-timezone
688 (format-time-string "%FT%T%z" time-internal)))
689
690 ;; Use month day year order to be compatible with
691 ;; calendar-cursor-to-date. I wish I could instead use the ISO 8601
692 ;; ordering, year month day.
693 (defun exco-get-meetings-for-day (identifier month day year callback)
694 "Return the meetings for the specified day.
695 IDENTIFIER is the connection identifier. MONTH, DAY and YEAR are
696 the meeting month, day and year. Call CALLBACK with two
697 arguments, IDENTIFIER and the server's response."
698 (let* ((start-of-day-time-internal
699 (apply #'encode-time `(0 0 0 ,day ,month ,year)))
700 (start-of-day-date-time
701 (exco-format-date-time start-of-day-time-internal))
702 (start-of-next-day-date-time
703 (exco-extend-timezone
704 (format-time-string "%FT00:00:00%z"
705 (time-add start-of-day-time-internal
706 (seconds-to-time 86400))))))
707 (exco-operate
708 identifier
709 "FindItem"
710 `(;; Main arguments.
711 ((Traversal . "Shallow")
712 (ItemShape
713 (BaseShape . "AllProperties"))
714 ;; To aid productivity, excorporate-calfw automatically prunes your
715 ;; meetings to a maximum of 100 per day.
716 (CalendarView (MaxEntriesReturned . "100")
717 (StartDate . ,start-of-day-date-time)
718 (EndDate . ,start-of-next-day-date-time))
719 (ParentFolderIds
720 (DistinguishedFolderId (Id . "calendar"))))
721 ;; Empty arguments.
722 ,@(let ((server-major-version
723 (string-to-number
724 (substring (exco-server-version identifier) 8 12))))
725 (cond
726 ((<= server-major-version 2007)
727 '(nil nil nil nil))
728 ((< server-major-version 2013)
729 '(nil nil nil nil nil))
730 (t
731 '(nil nil nil nil nil nil)))))
732 callback)))
733
734 (defun exco-connection-iterate (initialize-function
735 per-connection-function
736 per-connection-callback
737 finalize-function)
738 "Iterate Excorporate connections.
739 Call INITIALIZE-FUNCTION once before iterating.
740 Call PER-CONNECTION-FUNCTION for each connection.
741 Pass PER-CONNECTION-CALLBACK to PER-CONNECTION-FUNCTION.
742 Call FINALIZE-FUNCTION after all operations have responded."
743 (exco--ensure-connection)
744 (funcall initialize-function)
745 (let ((responses 0)
746 (connection-count (length exco--connection-identifiers)))
747 (dolist (identifier exco--connection-identifiers)
748 (funcall per-connection-function identifier
749 (lambda (&rest arguments)
750 (setq responses (1+ responses))
751 (apply per-connection-callback arguments)
752 (when (equal responses connection-count)
753 (funcall finalize-function)))))))
754 \f
755 ;; User-visible functions and variables.
756 (defgroup excorporate nil
757 "Exchange support."
758 :version "25.1"
759 :group 'comm
760 :group 'calendar)
761
762 ;; Name the excorporate-configuration variable vaguely. It is currently a
763 ;; MAIL-ADDRESS string, a pair (MAIL-ADDRESS . SERVICE-URL), or nil. In the
764 ;; future it could allow a list of strings and pairs.
765 (defcustom excorporate-configuration nil
766 "Excorporate configuration.
767 The mail address to use for autodiscovery."
768 :type '(choice
769 (const
770 :tag "Prompt for Exchange mail address to use for autodiscovery" nil)
771 (string :tag "Exchange mail address to use for autodiscovery")
772 (cons :tag "Skip autodiscovery"
773 (string :tag "Exchange mail address (e.g., hacker@gnu.org)")
774 (string :tag "Exchange Web Services URL\
775 (e.g., https://mail.gnu.org/ews/exchange.asmx)"))))
776
777 ;;;###autoload
778 (defun excorporate ()
779 "Start Excorporate.
780 Prompt for a mail address to use for autodiscovery, with an
781 initial suggestion of `user-mail-address'. However, if
782 `excorporate-configuration' is non-nil, `excorporate' will use
783 that without prompting."
784 (interactive)
785 (cond
786 ((eq excorporate-configuration nil)
787 (exco-connect (completing-read "Exchange mail address: "
788 (list user-mail-address)
789 nil nil user-mail-address)))
790 ((stringp excorporate-configuration)
791 (exco-connect excorporate-configuration))
792 ((null (consp (cdr excorporate-configuration)))
793 (exco-connect excorporate-configuration))
794 (t
795 (error "Excorporate: Invalid configuration"))))
796
797 (provide 'excorporate)
798
799 ;;; excorporate.el ends here