emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] master 673026e: packages/excorporate: New package, import version


From: Thomas Fitzsimmons
Subject: [elpa] master 673026e: packages/excorporate: New package, import version 0.7.0
Date: Thu, 25 Feb 2016 02:41:20 +0000

branch: master
commit 673026e59ce120808f3bca133c3c43a34a134d23
Author: Thomas Fitzsimmons <address@hidden>
Commit: Thomas Fitzsimmons <address@hidden>

    packages/excorporate: New package, import version 0.7.0
    
    * packages/excorporate/README,
    packages/excorporate/excorporate-calendar.el,
    packages/excorporate/excorporate-calfw.el.txt,
    packages/excorporate/excorporate-org.el,
    packages/excorporate/excorporate.el: New files.
---
 packages/excorporate/README                   |   20 +
 packages/excorporate/excorporate-calendar.el  |   46 ++
 packages/excorporate/excorporate-calfw.el.txt |  128 ++++
 packages/excorporate/excorporate-org.el       |  141 +++++
 packages/excorporate/excorporate.el           |  786 +++++++++++++++++++++++++
 5 files changed, 1121 insertions(+), 0 deletions(-)

diff --git a/packages/excorporate/README b/packages/excorporate/README
new file mode 100644
index 0000000..7389a88
--- /dev/null
+++ b/packages/excorporate/README
@@ -0,0 +1,20 @@
+Excorporate provides Exchange integration for Emacs.
+
+To create a connection to a web service:
+
+M-x excorporate
+
+Excorporate will prompt for an email address that it will use to
+automatically discover settings.  Then it will prompt you for your
+credentials two or three times depending on the server configuration.
+
+You should see a message indicating that the connection is ready
+either in the minibuffer or in the *Messages* buffer.
+
+Finally, run M-x calendar, and press 'e' to show today's meetings.
+
+If autodiscovery fails, customize `excorporate-configuration' to skip
+autodiscovery.
+
+For further information including connection troubleshooting, see the
+Excorporate Info node at C-h i d m Excorporate.
diff --git a/packages/excorporate/excorporate-calendar.el 
b/packages/excorporate/excorporate-calendar.el
new file mode 100644
index 0000000..506ac72
--- /dev/null
+++ b/packages/excorporate/excorporate-calendar.el
@@ -0,0 +1,46 @@
+;;; excorporate-calendar.el --- Exchange for calendar -*- lexical-binding: t 
-*-
+
+;; Copyright (C) 2014-2016 Free Software Foundation, Inc.
+
+;; Author: Thomas Fitzsimmons <address@hidden>
+;; Keywords: calendar
+
+;; 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, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Add a calendar keybinding for Excorporate.  Default to the
+;; excorporate-org interface.
+
+;;; Code:
+
+(require 'calendar)
+
+(defcustom excorporate-calendar-show-day-function 'exco-org-show-day
+  "A function to be called by pressing `e' in Calendar."
+  :type 'function
+  :group 'excorporate)
+
+(defun exco-calendar-show-day ()
+  "Show meetings for the selected date."
+  (interactive)
+  (apply excorporate-calendar-show-day-function (calendar-cursor-to-date t)))
+
+;; I arrogantly claim "e" for now, but irresponsibly reserve the right
+;; to change it later.
+(define-key calendar-mode-map "e" #'exco-calendar-show-day)
+
+(provide 'excorporate-calendar)
+
+;;; excorporate-calendar.el ends here
diff --git a/packages/excorporate/excorporate-calfw.el.txt 
b/packages/excorporate/excorporate-calfw.el.txt
new file mode 100644
index 0000000..ad31ae9
--- /dev/null
+++ b/packages/excorporate/excorporate-calfw.el.txt
@@ -0,0 +1,128 @@
+;;; excorporate-calfw.el --- Exchange calendar view   -*- lexical-binding: t 
-*-
+
+;; Copyright (C) 2014-2016 Free Software Foundation, Inc.
+
+;; Author: Thomas Fitzsimmons <address@hidden>
+;; Keywords: calendar
+
+;; 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, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Use the Calfw calendar framework to display daily meetings.
+
+;; To use this handler, set excorporate-calendar-show-day to
+;; exco-calfw-show-day using `customize-variable'.
+
+;; This Excorporate handler requires the Calfw package, which is not
+;; included in GNU ELPA because not all Calfw contributors have
+;; copyright assignment papers on file with the FSF.
+
+;;; Code:
+
+(require 'calfw)
+(require 'excorporate)
+
+(defvar excorporate-calfw-buffer-name "*Excorporate*"
+  "The buffer into which Calfw output is inserted.")
+
+(defun exco-calfw-initialize-buffer (month day year)
+  "Set up an initial blank Calfw buffer for date MONTH DAY YEAR."
+  (with-current-buffer (get-buffer-create excorporate-calfw-buffer-name)
+    (display-buffer (current-buffer))
+    (let ((status-source (make-cfw:source :name "Updating..."
+                                         :data (lambda (_b _e) nil))))
+      (cfw:create-calendar-component-buffer
+       :date (cfw:date month day year) :view 'day
+       :contents-sources (list status-source)
+       :buffer (current-buffer)))))
+
+(defun exco-calfw-add-meeting (subject start end location
+                                      main-invitees optional-invitees)
+  "Add a scheduled meeting to the event list.
+SUBJECT is a string, the subject of the meeting.  START is the
+meeting start time in Emacs internal date time format, and END is
+the end of the meeting in the same format.  LOCATION is a string
+representing the location.  MAIN-INVITEES and OPTIONAL-INVITEES
+are the requested participants."
+  (let ((start-list (decode-time start))
+       (end-list (decode-time end)))
+    (make-cfw:event :title (concat
+                           (format "\n\t%s" subject)
+                           (format "\n\tLocation: %s" location)
+                           (format "\n\tInvitees: %s"
+                                   (mapconcat 'identity
+                                              main-invitees
+                                              "; "))
+                           (when optional-invitees
+                             (format "\n\tOptional: %s"
+                                     (mapconcat 'identity
+                                                optional-invitees "; "))))
+                   :start-date  (list (elt start-list 4)
+                                      (elt start-list 3)
+                                      (elt start-list 5))
+                   :start-time  (list (elt start-list 2)
+                                      (elt start-list 1))
+                   :end-date    (list (elt end-list 4)
+                                      (elt end-list 3)
+                                      (elt end-list 5))
+                   :end-time    (list (elt end-list 2)
+                                      (elt end-list 1)))))
+
+(defun exco-calfw-add-meetings (identifier response)
+  "Add the connection IDENTIFIER's meetings from RESPONSE."
+  (let ((event-list (exco-calendar-item-iterate response
+                                               #'exco-calfw-add-meeting)))
+    (with-current-buffer (get-buffer-create excorporate-calfw-buffer-name)
+      (declare (special cfw:component))
+      (let* ((new-source (make-cfw:source
+                         :name (format "%S (as of %s)"
+                                       identifier
+                                       (format-time-string "%F %H:%M"))
+                         :data (lambda (_b _e)
+                                 event-list)))
+            (sources (cfw:cp-get-contents-sources cfw:component))
+            (new-sources (append sources (list new-source))))
+       (cfw:cp-set-contents-sources cfw:component new-sources)))))
+
+(defun exco-calfw-finalize-buffer ()
+  "Finalize the Calfw widget after retrievals have completed."
+  (with-current-buffer (get-buffer-create excorporate-calfw-buffer-name)
+    (declare (special cfw:component))
+    (let ((sources (cfw:cp-get-contents-sources cfw:component))
+         (status-source (make-cfw:source :name "Done."
+                                         :data (lambda (_b _e) nil))))
+      (cfw:cp-set-contents-sources cfw:component
+                                  (cons status-source (cdr sources))))
+    (cfw:cp-add-selection-change-hook cfw:component
+                                     (lambda ()
+                                       (apply #'exco-calfw-show-day
+                                              (cfw:cursor-to-nearest-date))))
+    (cfw:refresh-calendar-buffer nil)))
+
+;;;###autoload
+(defun exco-calfw-show-day (month day year)
+  "Show meetings for the date specified by MONTH DAY YEAR."
+  (exco-connection-iterate
+   (lambda ()
+     (exco-calfw-initialize-buffer month day year))
+   (lambda (identifier callback)
+     (exco-get-meetings-for-day identifier month day year
+                               callback))
+   #'exco-calfw-add-meetings
+   #'exco-calfw-finalize-buffer))
+
+(provide 'excorporate-calfw)
+
+;;; excorporate-calfw.el ends here
diff --git a/packages/excorporate/excorporate-org.el 
b/packages/excorporate/excorporate-org.el
new file mode 100644
index 0000000..8613f8e
--- /dev/null
+++ b/packages/excorporate/excorporate-org.el
@@ -0,0 +1,141 @@
+;;; excorporate-org.el --- Exchange Org Mode view     -*- lexical-binding: t 
-*-
+
+;; Copyright (C) 2016 Free Software Foundation, Inc.
+
+;; Author: Thomas Fitzsimmons <address@hidden>
+;; Keywords: calendar
+
+;; 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, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Use the Org Mode to display daily meetings.
+
+;;; Code:
+
+(require 'org)
+(require 'excorporate)
+
+(defvar excorporate-org-buffer-name "*Excorporate*"
+  "The buffer into which Org Mode output is inserted.")
+
+(defun exco-org-initialize-buffer ()
+  "Add initial text to the destination buffer."
+  (with-current-buffer (get-buffer-create excorporate-org-buffer-name)
+      (setq buffer-read-only t)
+      (org-mode)
+      (display-buffer (current-buffer))
+      (let ((inhibit-read-only t))
+       (delete-region (point-min) (point-max))
+       (goto-char 1)
+       (insert "# Updated...\n"))))
+
+(defun exco-org-format-headline (identifier)
+  "Format an Org headline using IDENTIFIER."
+  (format "* Calendar (%s)\n" identifier))
+
+(defun exco-org-insert-meeting-headline (subject start-time end-time)
+  "Insert and schedule a meeting.
+SUBJECT is the meeting's subject, START-TIME and END-TIME are the
+meeting's start and end times in the same format as is returned
+by `current-time'."
+  (let* ((now (current-time))
+        (keyword (if (time-less-p now end-time)
+                     "TODO"
+                   "DONE")))
+    (insert (format "** %s %s\n" keyword subject))
+    (org-schedule nil (format-time-string "<%Y-%m-%d %a %H:%M>"
+                                         start-time))
+    (forward-line -1)
+    (end-of-line)
+    (insert  "--" (format-time-string "<%Y-%m-%d %a %H:%M>" end-time))
+    (forward-line)
+    (org-insert-time-stamp (current-time) t t "+ Retrieved " "\n")))
+
+(defun exco-org-insert-invitees (invitees)
+  "Parse and insert a list of invitees, INVITEES."
+  (dolist (invitee invitees)
+    (insert (format "  + %s\n" invitee))))
+
+(defun exco-org-insert-headline (identifier month day year)
+  "Insert Org headline for IDENTIFIER on date MONTH DAY YEAR."
+  (with-current-buffer (get-buffer-create excorporate-org-buffer-name)
+    (let ((inhibit-read-only t))
+      (insert (exco-org-format-headline identifier))
+      (org-insert-time-stamp (encode-time 0 0 0 day month year)
+                            nil t "  + Date " "\n"))))
+
+(defun exco-org-insert-meeting (subject start end location
+                                       main-invitees optional-invitees)
+  "Insert a scheduled meeting.
+SUBJECT is a string, the subject of the meeting.  START is the
+meeting start time in Emacs internal date time format, and END is
+the end of the meeting in the same format.  LOCATION is a string
+representing the location.  MAIN-INVITEES and OPTIONAL-INVITEES
+are the requested participants."
+  (exco-org-insert-meeting-headline subject start end)
+  (insert (format "+ Duration: %d minutes\n"
+                 (round (/ (float-time (time-subtract end start)) 60.0))))
+  (insert (format "+ Location: %s\n" location))
+  (insert "+ Invitees:\n")
+  (exco-org-insert-invitees main-invitees)
+  (when optional-invitees
+    (insert "+ Optional invitees:\n")
+    (exco-org-insert-invitees optional-invitees)))
+
+(defun exco-org-insert-meetings (identifier response)
+  "Insert the connection IDENTIFIER's meetings from RESPONSE."
+  (with-current-buffer (get-buffer-create excorporate-org-buffer-name)
+    (let ((inhibit-read-only t)
+         (name-regexp (concat "\\" (exco-org-format-headline identifier))))
+      (goto-char 1)
+      (end-of-line)
+      (insert (format "%s..." identifier))
+      (goto-char (point-max))
+      (re-search-backward name-regexp nil)
+      (forward-line 2)
+      (org-insert-time-stamp (current-time) t t "  + Last checked " "\n")
+      (exco-calendar-item-iterate response #'exco-org-insert-meeting)
+      (re-search-backward name-regexp nil)
+      (if (save-excursion (org-goto-first-child))
+         (org-sort-entries t ?s)
+       (forward-line 3)
+       (insert "`♘")))))
+
+(defun exco-org-finalize-buffer ()
+  "Finalize text in buffer after all connections have responded."
+  (with-current-buffer (get-buffer-create excorporate-org-buffer-name)
+    ;; Sort top-level entries alphabetically.
+    (let ((inhibit-read-only t))
+      (goto-char (point-min))
+      (end-of-line)
+      (insert "done.")
+      (org-sort-entries t ?a))))
+
+;;;###autoload
+(defun exco-org-show-day (month day year)
+  "Show meetings for the date specified by MONTH DAY YEAR."
+  (exco-connection-iterate #'exco-org-initialize-buffer
+                          (lambda (identifier callback)
+                            (exco-org-insert-headline identifier
+                                                      month day year)
+                            (exco-get-meetings-for-day identifier
+                                                       month day year
+                                                       callback))
+                          #'exco-org-insert-meetings
+                          #'exco-org-finalize-buffer))
+
+(provide 'excorporate-org)
+
+;;; excorporate-org.el ends here
diff --git a/packages/excorporate/excorporate.el 
b/packages/excorporate/excorporate.el
new file mode 100644
index 0000000..80f3c33
--- /dev/null
+++ b/packages/excorporate/excorporate.el
@@ -0,0 +1,786 @@
+;;; excorporate.el --- Exchange integration           -*- lexical-binding: t 
-*-
+
+;; Copyright (C) 2014-2016 Free Software Foundation, Inc.
+
+;; Author: Thomas Fitzsimmons <address@hidden>
+;; Maintainer: Thomas Fitzsimmons <address@hidden>
+;; Created: 2014-09-19
+;; Version: 0.7.0
+;; Keywords: calendar
+;; Homepage: https://www.fitzsim.org/blog/
+;; Package-Requires: ((emacs "24.1") (fsm "0.2") (soap-client "3.0.2") 
(url-http-ntlm "2.0.2"))
+
+;; 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, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Excorporate provides Exchange integration for Emacs.
+
+;; To create a connection to a web service:
+
+;; M-x excorporate
+
+;; Excorporate will prompt for an email address that it will use to
+;; automatically discover settings.  Then it will connect to two or
+;; three separate hosts: the autodiscovery host, the web service host
+;; or load balancer, and the actual server if there is a load
+;; balancer.  Therefore you may be prompted for your credentials two
+;; or three times.
+
+;; You should see a message indicating that the connection is ready
+;; either in the minibuffer or failing that in the *Messages* buffer.
+
+;; Finally, run M-x calendar, and press 'e' to show today's meetings.
+
+;; Please try autodiscovery first and report issues not yet listed
+;; below.  When autodiscovery works it is very convenient; the goal is
+;; to make it work for as many users as possible.
+
+;; If autodiscovery fails, customize `excorporate-configuration' to
+;; skip autodiscovery.
+
+;; Autodiscovery will fail if:
+
+;; - Excorporate is accessing the server through a proxy (Emacs
+;;   bug#10).
+
+;; - The server is not configured to support autodiscovery.
+
+;; - The email address is at a different domain than the server, e.g.,
+;;   address@hidden, autodiscover.domain2.com.
+
+;; - Authentication is Kerberos/GSSAPI.
+
+;; Excorporate does know about the special case where the mail address
+;; is at a subdomain, e.g., address@hidden, and the server is at
+;; the main domain, e.g., autodiscover.domain.com.  Autodiscovery will
+;; work in that case.
+
+;; Excorporate must be loaded before any other package that requires
+;; `soap-client'.  The version of `soap-client' that Excorporate
+;; bundles is backward compatible.
+
+;; Acknowledgments:
+
+;; Alexandru Harsanyi <address@hidden> provided help and
+;; guidance on how to extend soap-client.el's WSDL and XSD handling,
+;; enabling support for the full Exchange Web Services API.
+
+;; Alex Luccisano <address@hidden> tested early versions of
+;; this library against a corporate installation of Exchange.
+
+;; Jon Miller <address@hidden> tested against Exchange 2013.  He
+;; also tracked down and reported a bad interaction with other
+;; packages that require soap-client.
+
+;; Nicolas Lamirault <address@hidden> tested the
+;; autodiscovery feature.
+
+;; Trey Jackson <address@hidden> confirmed autodiscovery worked
+;; for him.
+
+;; Joakim Verona <address@hidden> tested autodiscovery in a
+;; Kerberos/GSSAPI environment.
+
+;; Wilfred Hughes <address@hidden> tested on Exchange 2007 and
+;; suggested documentation improvements.
+
+;;; Code:
+
+;; Implementation-visible functions and variables.
+
+;; Add NTLM authorization scheme.
+(require 'url-http-ntlm)
+(require 'soap-client)
+(require 'fsm)
+(require 'excorporate-calendar)
+
+(defconst exco--autodiscovery-templates
+  '("https://%s/autodiscover/autodiscover.svc";
+    "https://autodiscover.%s/autodiscover/autodiscover.svc";)
+  "Autodiscovery URL templates.
+URL templates to be formatted with a domain name, then searched
+for autodiscovery files.")
+
+(defvar exco--connections nil
+  "A hash table of finite state machines.
+The key is the identifier passed to `exco-connect'.  Each finite
+state machine represents a service connection.")
+
+(defvar exco--connection-identifiers nil
+  "An ordered list of connection identifiers.")
+
+(defun exco--parse-xml-in-current-buffer ()
+  "Decode and parse the XML contents of the current buffer."
+  (let ((mime-part (mm-dissect-buffer t t)))
+    (unless mime-part
+      (error "Failed to decode response from server"))
+    (unless (equal (car (mm-handle-type mime-part)) "text/xml")
+      (error "Server response is not an XML document"))
+    (with-temp-buffer
+      (mm-insert-part mime-part)
+      (prog1
+         (car (xml-parse-region (point-min) (point-max)))
+       (kill-buffer)
+       (mm-destroy-part mime-part)))))
+
+(defun exco--bind-wsdl (wsdl service-url port-name target-namespace
+                            binding-name)
+  "Create a WSDL binding.
+Create a binding port for WSDL from SERVICE-URL, PORT-NAME,
+TARGET-NAMESPACE and BINDING-NAME."
+  (let* ((namespace (soap-wsdl-find-namespace target-namespace wsdl))
+        (port (make-soap-port
+               :name port-name
+               :binding (cons target-namespace binding-name)
+               :service-url service-url)))
+    (soap-namespace-put port namespace)
+    (push port (soap-wsdl-ports wsdl))
+    (soap-resolve-references port wsdl)
+    wsdl))
+
+(defun exco--handle-url-error (url status)
+  "Handle an error that occurred when retrieving URL.
+The details of the error are in STATUS, in the same format as the
+argument to a `url-retrieve' callback.  Return non-nil to retry,
+nil to continue."
+  (if (eq (cl-third (plist-get status :error)) 500)
+      ;; The server reported an internal server error.  Try to recover
+      ;; by re-requesting the target URL and its most recent redirect.
+      ;; I'm not sure what conditions cause the server to get into
+      ;; this state -- it might be because the server has stale
+      ;; knowledge of old keepalive connections -- but this should
+      ;; recover it.  We need to disable ntlm in
+      ;; url-registered-auth-schemes so that it doesn't prevent
+      ;; setting keepalives to nil.
+      (let ((url-registered-auth-schemes nil)
+           (url-http-attempt-keepalives nil)
+           (redirect (plist-get status :redirect)))
+       (fsm-debug-output "exco--fsm received 500 error for %s" url)
+       (url-debug 'excorporate "Attempting 500 recovery")
+       (ignore-errors
+         ;; Emacs's url-retrieve does not respect the values of
+         ;; url-http-attempt-keepalives and
+         ;; url-registered-auth-schemes in asynchronous contexts.
+         ;; Unless url.el is eventually changed to do so, the
+         ;; following requests must be synchronous so that they run
+         ;; entirely within url-http-attempt-keepalives's dynamic
+         ;; extent.  These calls block the main event loop,
+         ;; unfortunately, but only in this rare error recovery
+         ;; scenario.
+         (url-retrieve-synchronously url)
+         (when redirect (url-retrieve-synchronously redirect)))
+       (url-debug 'excorporate "Done 500 recovery attempt")
+       ;; Retry.
+       t)
+    ;; We received some other error, which just
+    ;; means we should try the next URL.
+    (fsm-debug-output "exco--fsm didn't find %s" url)
+    ;; Don't retry.
+    nil))
+
+(defun exco--retrieve-next-import (fsm state-data return-for next-state)
+  "Retrieve the next XML schema import.
+FSM is the finite state machine, STATE-DATA is FSM's state data,
+and RETURN-FOR is one of :enter or :event to indicate what return
+type the calling function expects.  NEXT-STATE is the next state
+the FSM should transition to on success."
+  (let* ((url (plist-get state-data :service-url))
+        (xml (plist-get state-data :service-xml))
+        (wsdl (plist-get state-data :service-wsdl))
+        (imports (soap-wsdl-xmlschema-imports wsdl))
+        (next-state (if imports :parsing-service-wsdl next-state)))
+    (when imports
+      (let ((import-url (url-expand-file-name (pop imports) url)))
+       (let ((url-request-method "GET")
+             (url-package-name "soap-client.el")
+             (url-package-version "1.0")
+             (url-mime-charset-string "utf-8;q=1, iso-8859-1;q=0.5")
+             (url-http-attempt-keepalives t))
+         (url-retrieve
+          import-url
+          (lambda (status)
+            (let ((data-buffer (current-buffer)))
+              (unwind-protect
+                  (progn
+                    (url-debug 'excorporate "Processing import %s" status)
+                    (if (eq (car status) :error)
+                        ;; There is an error.  It may be recoverable
+                        ;; if it's HTTP 500 (internal server error).
+                        (if (and (exco--handle-url-error import-url status)
+                                 ;; Only retry once.
+                                 (not (plist-get state-data :retrying)))
+                            ;; We should retry.  Don't save the
+                            ;; popped urls list to state-data, so
+                            ;; that this :try-next-url will
+                            ;; re-attempt to retrieve the same car as
+                            ;; before.  Set the retry flag.
+                            (progn
+                              (plist-put state-data :retrying t))
+                          ;; Save the popped urls list so that the next url
+                          ;; is attempted, and clear the retry flag.
+                          (plist-put state-data :retrying nil)
+                          (setf (soap-wsdl-xmlschema-imports wsdl) imports)
+                          (plist-put state-data :failure-message
+                                     (format "Failed to retrieve %s"
+                                             import-url))
+                          (fsm-send fsm :unrecoverable-error))
+                      ;; Success, parse WSDL.
+                      (plist-put state-data :retrying nil)
+                      (setf (soap-wsdl-xmlschema-imports wsdl) imports)
+                      (soap-with-local-xmlns xml
+                        (soap-wsdl-add-namespace
+                         (soap-parse-schema (soap-parse-server-response) wsdl)
+                         wsdl))
+                      (plist-put state-data :service-wsdl wsdl)))
+                (and (buffer-live-p data-buffer)
+                     (kill-buffer data-buffer))))
+            (fsm-send fsm t))))))
+    (if (eq return-for :enter)
+       (list state-data nil)
+      (list next-state state-data nil))))
+
+(define-state-machine exco--fsm :start
+  ((identifier)
+   "Start an Excorporate finite state machine."
+   (if (stringp identifier)
+       (let ((domain (cadr (split-string identifier "@"))))
+        (unless (and domain (not (equal domain "")))
+          (error "Invalid domain for address %s" identifier))
+        (list :retrieving-autodiscovery-xml
+              (list
+               ;; State machine data.
+               ;; Unique finite state machine identifier.  Either mail-address
+               ;; or (mail-address . service-url).  The latter allows multiple
+               ;; state machines to operate on the same service URL.  Login
+               ;; credentials are handled separately by auth-source and url,
+               ;; so these should be the only two identifier types needed here.
+               :identifier identifier
+               ;; User data.
+               :mail-address identifier
+               ;; Error recovery data.
+               :retrying nil
+               ;; Autodiscovery data.
+               :autodiscovery-urls
+               (append (mapcar (lambda (template)
+                                 (format template domain))
+                               exco--autodiscovery-templates)
+                       ;; Handle the address@hidden =>
+                       ;; autodiscover.domain.com case reported by a
+                       ;; user.  Only try one extra level.
+                       (let ((domain-parts (split-string domain "\\.")))
+                         (when (> (length domain-parts) 2)
+                           (mapcar (lambda (template)
+                                     (format template
+                                             (mapconcat
+                                              'identity
+                                              (cdr domain-parts) ".")))
+                                   exco--autodiscovery-templates))))
+               ;; Service data.
+               :service-url nil
+               :service-xml nil
+               :service-wsdl nil
+               ;; State data.
+               :next-state-after-success nil
+               :failure-message nil
+               :server-version nil)
+              ;; No timeout.
+              nil))
+     ;; Go directly to :retrieving-service-xml, skipping autodiscovery.
+     (list :retrieving-service-xml
+          (list
+           :identifier identifier
+           :mail-address (car identifier)
+           :retrying nil
+           :autodiscovery-urls nil
+           ;; Use service-url field from identifier.
+           :service-url (cdr identifier)
+           :service-xml nil
+           :service-wsdl nil
+           :next-state-after-success nil
+           :failure-message nil
+           :server-version nil)
+          ;; No timeout.
+          nil))))
+
+(define-state exco--fsm :retrieving-autodiscovery-xml
+  (fsm state-data event _callback)
+  (cl-case event
+    (:try-next-url
+     (let ((urls (plist-get state-data :autodiscovery-urls)))
+       (if urls
+          (let ((url (pop urls)))
+            (fsm-debug-output "exco--fsm will probe %s" url)
+            (condition-case nil
+                (url-retrieve
+                 url
+                 (lambda (status)
+                   (let ((data-buffer (current-buffer)))
+                     (unwind-protect
+                         (progn
+                           (url-debug 'excorporate
+                            "Processing status: %s" status)
+                           (if (eq (car status) :error)
+                               (progn
+                                 (if (and
+                                      (exco--handle-url-error url status)
+                                      ;; Only retry once.
+                                      (not (plist-get state-data :retrying)))
+                                     ;; We should retry.  Don't save the popped
+                                     ;; urls list to state-data, so that this
+                                     ;; :try-next-url will re-attempt to
+                                     ;; retrieve the same car as before.  Set
+                                     ;; the retry flag.
+                                     (plist-put state-data :retrying t)
+                                   ;; Save the popped urls list so that the
+                                   ;; next url is attempted, and clear the
+                                   ;; retry flag.
+                                   (plist-put state-data :retrying nil)
+                                   (plist-put state-data
+                                              :autodiscovery-urls urls))
+                                 ;; Try next or retry.
+                                 (fsm-send fsm :try-next-url))
+                             ;; Success, save URL and parse returned XML.
+                             (message
+                              "Excorporate: Found autodiscovery URL for %S: %s"
+                              (plist-get state-data :identifier) url)
+                             (plist-put state-data :retrying nil)
+                             (plist-put state-data :service-url url)
+                             (plist-put state-data :service-xml
+                                        (exco--parse-xml-in-current-buffer))
+                             (fsm-send fsm :success))
+                           (url-debug 'excorporate "Done processing status"))
+                       (and (buffer-live-p data-buffer)
+                            (kill-buffer data-buffer))))))
+              (error
+               (fsm-debug-output "exco--fsm connection refused for %s" url)
+               (plist-put state-data :retrying nil)
+               (plist-put state-data :autodiscovery-urls urls)
+               (fsm-send fsm :try-next-url)))
+            (list :retrieving-autodiscovery-xml state-data nil))
+        (plist-put state-data :failure-message
+                   "Autodiscovery ran out of URLs to try")
+        (list :shutting-down-on-error state-data nil))))
+    (:success
+     (plist-put state-data :next-state-after-success :retrieving-service-xml)
+     (list :parsing-service-wsdl state-data nil))))
+
+(define-enter-state exco--fsm :shutting-down-on-error
+  (_fsm state-data)
+  (let ((failure-message (plist-get state-data :failure-message)))
+    (exco-disconnect (plist-get state-data :identifier))
+    (message "Excorporate: %s" failure-message)
+    (url-debug 'excorporate "Failed: %s" failure-message)
+    (fsm-debug-output "exco--fsm failed: %s" failure-message))
+  (list state-data nil))
+
+(define-state exco--fsm :shutting-down-on-error
+  (_fsm state-data _event _callback)
+  (list :shutting-down-on-error state-data nil))
+
+(define-enter-state exco--fsm :retrieving-service-xml
+  (fsm state-data)
+  (when (stringp (plist-get state-data :identifier))
+    (let* ((xml (plist-get state-data :service-xml))
+          (unbound-wsdl (plist-get state-data :service-wsdl))
+          (wsdl
+           (progn
+             ;; Skip soap-parse-wsdl-phase-fetch-schema to avoid
+             ;; synchronous URL fetches.
+             (soap-parse-wsdl-phase-finish-parsing xml unbound-wsdl)
+             (exco--bind-wsdl
+              (soap-wsdl-resolve-references unbound-wsdl)
+              (plist-get state-data :service-url)
+              "AutodiscoverServicePort"
+              "http://schemas.microsoft.com/exchange/2010/Autodiscover";
+              "DefaultBinding_Autodiscover"))))
+      (soap-invoke-async
+       (lambda (response)
+        (let ((result-url
+               (exco-extract-value '(Response
+                                     UserResponses
+                                     UserResponse
+                                     UserSettings
+                                     UserSetting
+                                     Value)
+                                   response)))
+          (if result-url
+              (progn
+                (plist-put state-data :service-url result-url)
+                (message "Excorporate: Found service URL for %S: %s"
+                         (plist-get state-data :identifier)
+                         (plist-get state-data :service-url)))
+            ;; No result.  Check for error.
+            (let ((error-message
+                   (exco-extract-value '(Response
+                                         UserResponses
+                                         UserResponse
+                                         ErrorMessage)
+                                       response)))
+              (if error-message
+                  (message "Excorporate: %s" error-message)
+                (message "Excorporate: Failed to find service URL"))))
+          (fsm-send fsm :retrieve-xml)))
+       nil
+       wsdl
+       "AutodiscoverServicePort"
+       "GetUserSettings"
+       `((RequestedServerVersion . "Exchange2010")
+        (Request
+         (Users
+          (User
+           (Mailbox . ,(plist-get state-data :mail-address))))
+         (RequestedSettings
+          (Setting . "InternalEwsUrl")))))))
+  (list state-data nil))
+
+(define-state exco--fsm :retrieving-service-xml
+  (fsm state-data event _callback)
+  (cl-case event
+    (:unrecoverable-error
+     (list :shutting-down-on-error state-data nil))
+    (:retrieve-xml
+     (let ((service-url (plist-get state-data :service-url)))
+       (url-retrieve (concat service-url "?wsdl")
+                    (lambda (status)
+                      (let ((data-buffer (current-buffer)))
+                        (unwind-protect
+                            (if (eq (car status) :error)
+                                (progn
+                                  (plist-put state-data :failure-message
+                                             (format "Failed to retrieve %s"
+                                                     service-url))
+                                  (fsm-send fsm :unrecoverable-error))
+                              (plist-put state-data
+                                         :service-xml
+                                         (exco--parse-xml-in-current-buffer))
+                              (fsm-send fsm :success))
+                          (and (buffer-live-p data-buffer)
+                               (kill-buffer data-buffer)))))))
+     (list :retrieving-service-xml state-data nil))
+    (:success
+     (plist-put state-data :next-state-after-success :retrieving-data)
+     (list :parsing-service-wsdl state-data nil))))
+
+(define-enter-state exco--fsm :parsing-service-wsdl
+  (fsm state-data)
+  (let* ((url (plist-get state-data :service-url))
+        (xml (plist-get state-data :service-xml))
+        (next-state (plist-get state-data :next-state-after-success))
+        (wsdl (soap-make-wsdl url)))
+    (soap-parse-wsdl-phase-validate-node xml)
+    ;; Skip soap-parse-wsdl-phase-fetch-imports to avoid synchronous
+    ;; fetches of import URLs.
+    (soap-parse-wsdl-phase-parse-schema xml wsdl)
+    (plist-put state-data :service-wsdl wsdl)
+    (exco--retrieve-next-import fsm state-data :enter next-state)))
+
+(define-state exco--fsm :parsing-service-wsdl
+  (fsm state-data event _callback)
+  (if (eq event :unrecoverable-error)
+      (list :shutting-down-on-error state-data nil)
+    (let ((next-state (plist-get state-data :next-state-after-success)))
+      (exco--retrieve-next-import fsm state-data :event next-state))))
+
+(defun exco--get-server-version (wsdl)
+  "Extract server version from WSDL."
+  (catch 'found
+    (dolist (attribute
+            (soap-xs-type-attributes
+             (soap-xs-element-type
+              (soap-wsdl-get
+               '("http://schemas.microsoft.com/exchange/services/2006/types";
+                 . "RequestServerVersion")
+               wsdl 'soap-xs-element-p))))
+      (when (equal (soap-xs-attribute-name attribute) "Version")
+       (throw 'found (soap-xs-attribute-default attribute))))
+    (warn "Excorporate: Failed to determine server version")
+    nil))
+
+(define-enter-state exco--fsm :retrieving-data
+  (_fsm state-data)
+  (let ((wsdl (plist-get state-data :service-wsdl))
+       (identifier (plist-get state-data :identifier)))
+    ;; Skip soap-parse-wsdl-phase-fetch-schema to avoid synchronous
+    ;; URL fetches.
+    (soap-parse-wsdl-phase-finish-parsing (plist-get state-data :service-xml)
+                                         wsdl)
+    (exco--bind-wsdl
+     (soap-wsdl-resolve-references wsdl)
+     (plist-get state-data :service-url)
+     "ExchangeServicePort"
+     "http://schemas.microsoft.com/exchange/services/2006/messages";
+     "ExchangeServiceBinding")
+    (plist-put state-data :server-version (exco--get-server-version wsdl))
+    (fsm-debug-output "exco--fsm %s server version is %s"
+                     identifier (exco-server-version identifier))
+    (message "Excorporate: Connection %S is ready" identifier))
+  (list state-data nil))
+
+(define-state exco--fsm :retrieving-data
+  (_fsm state-data event _callback)
+  (let* ((identifier (plist-get state-data :identifier))
+        (wsdl (plist-get state-data :service-wsdl))
+        (name (pop event))
+        (arguments (pop event))
+        (callback (pop event)))
+    (apply #'soap-invoke-async
+          (lambda (response)
+            (funcall callback identifier response))
+          nil
+          wsdl
+          "ExchangeServicePort"
+          name
+          arguments))
+  (list :retrieving-data state-data nil))
+
+(defun exco--ensure-connection ()
+  "Ensure at least one connection exists or throw an error."
+  (unless exco--connection-identifiers
+    (error "Excorporate: No connections exist.  Run M-x excorporate")))
+
+(defmacro exco--with-fsm (identifier &rest body)
+  "With `fsm' set to IDENTIFIER, run BODY.
+Run BODY with `fsm' set to the finite state machine specified by
+IDENTIFIER."
+  (declare (indent 1) (debug t))
+  `(progn
+     (exco--ensure-connection)
+     (let ((fsm (gethash ,identifier exco--connections)))
+       (unless fsm
+        (error "Excorporate: Connection %S does not exist" ,identifier))
+       ,@body)))
+
+;; Developer-visible functions and variables.
+
+(defun exco-api-version ()
+  "Return the Excorporate API version.
+Return a non-negative integer representing the current
+Excorporate application programming interface version.  Version 0
+is subject to change."
+  0)
+
+(defun exco-connect (identifier)
+  "Connect or reconnect to a web service.
+IDENTIFIER is the mail address to use for autodiscovery or a
+pair (mail-address . service-url)."
+  (if (stringp identifier)
+      (message "Excorporate: Starting autodiscovery for %S"
+              identifier))
+  (let ((fsm (start-exco--fsm identifier)))
+    (unless exco--connections
+      (setq exco--connections (make-hash-table :test 'equal)))
+    (when (gethash identifier exco--connections)
+      (exco-disconnect identifier))
+    (puthash identifier fsm exco--connections)
+    (push identifier exco--connection-identifiers)
+    (if (stringp identifier)
+       (fsm-send fsm :try-next-url)
+      (fsm-send fsm :retrieve-xml))
+    nil))
+
+(defun exco-operate (identifier name arguments callback)
+  "Execute a service operation asynchronously.
+IDENTIFIER is the connection identifier.  Execute operation NAME
+with ARGUMENTS then call CALLBACK with two arguments, IDENTIFIER
+and the server's response."
+  (exco--with-fsm identifier
+    (fsm-send fsm (list name arguments callback)))
+  nil)
+
+(defun exco-server-version (identifier)
+  "Return the server version for connection IDENTIFIER, as a string.
+Examples are \"Exchange2010\", \"Exchange2010_SP1\",
+\"Exchange2013\"."
+  (exco--with-fsm identifier
+    (plist-get (fsm-get-state-data fsm) :server-version)))
+
+(defun exco-disconnect (identifier)
+  "Disconnect from a web service.
+IDENTIFIER is the mail address used to look up the connection."
+  (exco--with-fsm identifier
+    (setq exco--connection-identifiers
+         (delete identifier exco--connection-identifiers))
+    (remhash identifier exco--connections))
+  nil)
+
+(defun exco-extract-value (path result)
+  "Extract the value at PATH from RESULT.
+PATH is an ordered list of node names."
+  (let ((values (nreverse (car result))))
+    (dolist (path-element path)
+      (setq values (assoc path-element values)))
+    (cdr values)))
+
+(defun exco-calendar-item-iterate (response callback)
+  "Iterate through calendar items in RESPONSE, calling CALLBACK on each.
+Returns a list of results from callback.  CALLBACK takes arguments:
+SUBJECT, a string, the subject of the meeting.
+START, the start date and time in Emacs internal representation.
+END, the start date and time in Emacs internal representation.
+LOCATION, the location of the meeting.
+MAIN-INVITEES, a list of strings representing required participants.
+OPTIONAL-INVITEES, a list of strings representing optional participants."
+  (let ((result-list '()))
+    (dolist (calendar-item (exco-extract-value '(ResponseMessages
+                                                FindItemResponseMessage
+                                                RootFolder
+                                                Items)
+                                              response))
+      (let* ((subject (cdr (assoc 'Subject calendar-item)))
+            (start (cdr (assoc 'Start calendar-item)))
+            (start-internal (apply #'encode-time
+                                   (soap-decode-date-time
+                                    start 'dateTime)))
+            (end (cdr (assoc 'End calendar-item)))
+            (end-internal (apply #'encode-time
+                                 (soap-decode-date-time
+                                  end 'dateTime)))
+            (location (cdr (assoc 'Location calendar-item)))
+            (to-invitees (cdr (assoc 'DisplayTo calendar-item)))
+            (main-invitees (mapcar 'org-trim (split-string to-invitees ";")))
+            (cc-invitees (cdr (assoc 'DisplayCc calendar-item)))
+            (optional-invitees (when cc-invitees
+                                 (mapcar 'org-trim
+                                         (split-string cc-invitees ";")))))
+       (push (funcall callback subject start-internal end-internal
+                      location main-invitees optional-invitees)
+             result-list)))
+    (nreverse result-list)))
+
+;; Date-time utility functions.
+(defun exco-extend-timezone (date-time-string)
+  "Add a colon to the timezone in DATE-TIME-STRING.
+DATE-TIME-STRING must be formatted as if returned by
+`format-time-string' with FORMAT-STRING \"%FT%T%z\".  Web
+services require the ISO8601 extended format of timezone, which
+includes the colon."
+  (concat
+   (substring date-time-string 0 22) ":" (substring date-time-string 22)))
+
+(defun exco-format-date-time (time-internal)
+  "Convert TIME-INTERNAL to an XSD compatible date-time string."
+  (exco-extend-timezone
+   (format-time-string "%FT%T%z" time-internal)))
+
+;; Use month day year order to be compatible with
+;; calendar-cursor-to-date.  I wish I could instead use the ISO 8601
+;; ordering, year month day.
+(defun exco-get-meetings-for-day (identifier month day year callback)
+  "Return the meetings for the specified day.
+IDENTIFIER is the connection identifier.  MONTH, DAY and YEAR are
+the meeting month, day and year.  Call CALLBACK with two
+arguments, IDENTIFIER and the server's response."
+  (let* ((start-of-day-time-internal
+         (apply #'encode-time `(0 0 0 ,day ,month ,year)))
+        (start-of-day-date-time
+         (exco-format-date-time start-of-day-time-internal))
+        (start-of-next-day-date-time
+         (exco-extend-timezone
+          (format-time-string "%FT00:00:00%z"
+                              (time-add start-of-day-time-internal
+                                        (seconds-to-time 86400))))))
+    (exco-operate
+     identifier
+     "FindItem"
+     `(;; Main arguments.
+       ((Traversal . "Shallow")
+       (ItemShape
+        (BaseShape . "AllProperties"))
+       ;; To aid productivity, excorporate-calfw automatically prunes your
+       ;; meetings to a maximum of 100 per day.
+       (CalendarView (MaxEntriesReturned . "100")
+                     (StartDate . ,start-of-day-date-time)
+                     (EndDate . ,start-of-next-day-date-time))
+       (ParentFolderIds
+        (DistinguishedFolderId (Id . "calendar"))))
+       ;; Empty arguments.
+       ,@(let ((server-major-version
+               (string-to-number
+                (substring (exco-server-version identifier) 8 12))))
+          (cond
+           ((<= server-major-version 2007)
+            '(nil nil nil nil))
+           ((< server-major-version 2013)
+            '(nil nil nil nil nil))
+           (t
+            '(nil nil nil nil nil nil)))))
+     callback)))
+
+(defun exco-connection-iterate (initialize-function
+                               per-connection-function
+                               per-connection-callback
+                               finalize-function)
+  "Iterate Excorporate connections.
+Call INITIALIZE-FUNCTION once before iterating.
+Call PER-CONNECTION-FUNCTION for each connection.
+Pass PER-CONNECTION-CALLBACK to PER-CONNECTION-FUNCTION.
+Call FINALIZE-FUNCTION after all operations have responded."
+  (exco--ensure-connection)
+  (funcall initialize-function)
+  (let ((responses 0)
+       (connection-count (length exco--connection-identifiers)))
+    (dolist (identifier exco--connection-identifiers)
+      (funcall per-connection-function identifier
+              (lambda (&rest arguments)
+                (setq responses (1+ responses))
+                (apply per-connection-callback arguments)
+                (when (equal responses connection-count)
+                  (funcall finalize-function)))))))
+
+;; User-visible functions and variables.
+(defgroup excorporate nil
+  "Exchange support."
+  :version "25.1"
+  :group 'comm
+  :group 'calendar)
+
+;; Name the excorporate-configuration variable vaguely.  It is currently a
+;; MAIL-ADDRESS string, a pair (MAIL-ADDRESS . SERVICE-URL), or nil.  In the
+;; future it could allow a list of strings and pairs.
+(defcustom excorporate-configuration nil
+  "Excorporate configuration.
+The mail address to use for autodiscovery."
+  :type '(choice
+         (const
+          :tag "Prompt for Exchange mail address to use for autodiscovery" nil)
+         (string :tag "Exchange mail address to use for autodiscovery")
+         (cons :tag "Skip autodiscovery"
+               (string :tag "Exchange mail address (e.g., address@hidden)")
+               (string :tag "Exchange Web Services URL\
+ (e.g., https://mail.gnu.org/ews/exchange.asmx)"))))
+
+;;;###autoload
+(defun excorporate ()
+  "Start Excorporate.
+Prompt for a mail address to use for autodiscovery, with an
+initial suggestion of `user-mail-address'.  However, if
+`excorporate-configuration' is non-nil, `excorporate' will use
+that without prompting."
+  (interactive)
+  (cond
+   ((eq excorporate-configuration nil)
+    (exco-connect (completing-read "Exchange mail address: "
+                                  (list user-mail-address)
+                                  nil nil user-mail-address)))
+   ((stringp excorporate-configuration)
+    (exco-connect excorporate-configuration))
+   ((null (consp (cdr excorporate-configuration)))
+    (exco-connect excorporate-configuration))
+   (t
+    (error "Excorporate: Invalid configuration"))))
+
+(provide 'excorporate)
+
+;;; excorporate.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]