Calendaring support for Exchange Server 2007+ via EWS
authorTerry Wilson <twilson@digium.com>
Mon, 24 May 2010 18:21:20 +0000 (18:21 +0000)
committerTerry Wilson <twilson@digium.com>
Mon, 24 May 2010 18:21:20 +0000 (18:21 +0000)
This commit adds support for calendaring with Exchange Server 2007+ via
Exchange Web Services. Full write support and for querying attendees. Many
thanks to Jan Kaláb for the feature.

(closes issue #17022)
Reported by: pitel
Patches:
      res_calendar_ews.c uploaded by pitel (license 1008)
Tested by: pitel, twilson

Review: https://reviewboard.asterisk.org/r/557/
Review: https://reviewboard.asterisk.org/r/668/

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@265317 65c4cc65-6c06-0410-ace0-fbb531ad65f3

CHANGES
CREDITS
configs/calendar.conf.sample
res/res_calendar.c
res/res_calendar_ews.c [new file with mode: 0644]

diff --git a/CHANGES b/CHANGES
index 7f033a6..1f0cd88 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -386,8 +386,10 @@ Calendaring for Asterisk
  * A new set of modules were added supporing calendar integration with Asterisk.
    Dialplan functions for reading from and writing to calendars are included,
    as well as the ability to execute dialplan logic upon calendar event notifications.
-   iCalendar, CalDAV, and Exchange Server calendars are supported (Exchange support
-   only tested on Exchange Server 2003 with no support for forms-based authentication).
+   iCalendar, CalDAV, and Exchange Server calendars (via res_calendar_exchange for
+   Exchange Server 2003 with no write or attendee support, and res_calendar_ews for
+   Exchange Server 2007+ with full write and attendee support) are supported (Exchange
+   2003 support does not support forms-based authentication).
 
 Call Completion Supplementary Services for Asterisk
 ---------------------------------------------------
diff --git a/CREDITS b/CREDITS
index 5861c43..27fcb75 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -204,6 +204,8 @@ Sean Bright - Snom call pickup, newt interface for menuselect, cdr_tds rewrite,
        countless other improvements, fixes, and good ideas.
        sean(AT)malleable.com
 
+Jan Kaláb - Calendaring support for Exchange Server 2007+ via Exchange Web Services.
+
 === OTHER CONTRIBUTIONS ===
 John Todd - Monkey sounds and associated teletorture prompt
 Michael Jerris - bug marshaling
index 5965a2b..359e4d9 100644 (file)
@@ -1,5 +1,5 @@
 ;[calendar1]
-;type = ical              ;  type of calendar--currently supported: ical, caldav, or exchange
+;type = ical              ;  type of calendar--currently supported: ical, caldav, exchange, or ews
 ;url = https://example.com/home/jdoe/Calendar/   ; URL to shared calendar (Zimbra example)
 ;user = jdoe              ; web username
 ;secret = supersecret     ; web password
@@ -7,7 +7,7 @@
 ;timeframe = 60           ; number of minutes of calendar data to pull for each refresh period
 ;                         ; should always be >= refresh
 ;
-; You can set up res_icalendar to execute a call upon an upcoming busy status
+; You can set up res_calendar to execute a call upon an upcoming busy status
 ; The following fields are available from the ${CALENDAR_EVENT(<field>)} dialplan function:
 ;
 ; summary     : The VEVENT Summary property or Exchange subject
 ;waittime = 30            ; How long to wait for an answer
 
 ;[calendar2]
-; Note: Exchange support has only been tested on Exchange Server 2003
-;       Forms-based authentication is not supported at this time
-;       Querying attendees is not supported with Exchange at this time
+; Note: Support for Exchange Server 2003 
 ;
-;type = exchange          ;  type of calendar--currently supported: ical, caldav, or exchange
+;type = exchange          ;  type of calendar--currently supported: ical, caldav, exchange, or ews
 ;url = https://example.com/exchange/jdoe   ; URL to MS Exchange OWA for user (usually includes exchange/user)
 ;user = jdoe              ; Exchange username
 ;secret = mysecret        ; Exchange password
 ;timeframe = 60           ; number of minutes of calendar data to pull for each refresh period
 ;                         ; should always be >= refresh
 ;
-; You can set up res_icalendar to execute a call upon an upcoming busy status
+; You can set up res_calendar to execute a call upon an upcoming busy status
+;autoreminder = 10        ; Override event-defined reminder before each busy status (in mins)
+;
+;channel = SIP/1234       ; Channel to dial
+;context = default        ; Context to connect to on answer
+;extension = 1234         ; Extension to connect to on answer
+;
+; or
+;
+;[calendar3]
+; Note: Support for Exchange Server 2007+
+;
+;type = ews               ; type of calendar--currently supported: ical, caldav, exchange, or ews
+;url = https://example.com/ews/Exchange.asmx ; URL to MS Exchange EWS
+;user = jdoe              ; Exchange username
+;secret = mysecret        ; Exchange password
+;refresh = 15             ; refresh calendar every n minutes
+;timeframe = 60           ; number of minutes of calendar data to pull for each refresh period
+;                         ; should always be >= refresh
+;
+; You can set up res_calendar to execute a call upon an upcoming busy status
 ;autoreminder = 10        ; Override event-defined reminder before each busy status (in mins)
 ;
 ;channel = SIP/1234       ; Channel to dial
@@ -60,8 +78,8 @@
 ;
 ;waittime = 30            ; How long to wait for an answer
 
-;[calendar3]
-;type = caldav            ;  type of calendar--currently supported: ical, caldav, or exchange
+;[calendar4]
+;type = caldav            ;  type of calendar--currently supported: ical, caldav, exchange, or ews
 ;url = https://www.google.com/calendar/dav/username@gmail.com/events/  ; Main GMail calendar (the trailing slash is significant!)
 ;user = jdoe@gmail.com    ; username
 ;secret = mysecret        ; password
@@ -69,7 +87,7 @@
 ;timeframe = 60           ; number of minutes of calendar data to pull for each refresh period
 ;                         ; should always be >= refresh
 ;
-; You can set up res_icalendar to execute a call upon an upcoming busy status
+; You can set up res_calendar to execute a call upon an upcoming busy status
 ;autoreminder = 10        ; Override event-defined reminder before each busy status (in mins)
 ;
 ;channel = SIP/1234       ; Channel to dial
index a8c5ee7..2e3c8e4 100644 (file)
@@ -1260,6 +1260,7 @@ static int calendar_write_exec(struct ast_channel *chan, const char *cmd, char *
        char *val_dup = NULL;
        struct ast_calendar *cal = NULL;
        struct ast_calendar_event *event = NULL;
+       struct timeval tv = ast_tvnow();
        AST_DECLARE_APP_ARGS(fields,
                AST_APP_ARG(field)[10];
        );
@@ -1325,6 +1326,14 @@ static int calendar_write_exec(struct ast_channel *chan, const char *cmd, char *
                }
        }
 
+       if (!event->start) {
+               event->start = tv.tv_sec;
+       }
+
+       if (!event->end) {
+               event->end = tv.tv_sec;
+       }
+
        if((ret = cal->tech->write_event(event))) {
                ast_log(LOG_WARNING, "Writing event to calendar '%s' failed!\n", cal->name);
        }
diff --git a/res/res_calendar_ews.c b/res/res_calendar_ews.c
new file mode 100644 (file)
index 0000000..bd6a1ee
--- /dev/null
@@ -0,0 +1,838 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2008 - 2009, Digium, Inc.
+ *
+ * Jan Kalab <pitlicek@gmail.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ * \brief Resource for handling MS Exchange Web Service calendars
+ */
+
+/*** MODULEINFO
+       <depend>neon</depend>
+***/
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include <neon/ne_request.h>
+#include <neon/ne_session.h>
+#include <neon/ne_uri.h>
+#include <neon/ne_socket.h>
+#include <neon/ne_auth.h>
+#include <neon/ne_xml.h>
+#include <neon/ne_xmlreq.h>
+#include <neon/ne_utils.h>
+
+#include "asterisk/module.h"
+#include "asterisk/calendar.h"
+#include "asterisk/lock.h"
+#include "asterisk/config.h"
+#include "asterisk/astobj2.h"
+
+static void *ewscal_load_calendar(void *data);
+static void *unref_ewscal(void *obj);
+static int ewscal_write_event(struct ast_calendar_event *event);
+
+static struct ast_calendar_tech ewscal_tech = {
+       .type = "ews",
+       .description = "MS Exchange Web Service calendars",
+       .module = AST_MODULE,
+       .load_calendar = ewscal_load_calendar,
+       .unref_calendar = unref_ewscal,
+       .write_event = ewscal_write_event,
+};
+
+enum xml_op {
+       XML_OP_FIND = 100,
+       XML_OP_GET,
+       XML_OP_CREATE,
+};
+
+struct calendar_id {
+       struct ast_str *id;
+       AST_LIST_ENTRY(calendar_id) next;
+};
+
+struct xml_context {
+       ne_xml_parser *parser;
+       struct ast_str *cdata;
+       struct ast_calendar_event *event;
+       enum xml_op op;
+       struct ewscal_pvt *pvt;
+       AST_LIST_HEAD_NOLOCK(ids, calendar_id) ids;
+};
+
+/* Important states of XML parsing */
+enum {
+       XML_EVENT_NAME = 10,
+       XML_EVENT_START,
+       XML_EVENT_END,
+       XML_EVENT_BUSY,
+       XML_EVENT_ORGANIZER,
+       XML_EVENT_LOCATION,
+       XML_EVENT_ATTENDEE_LIST,
+       XML_EVENT_ATTENDEE,
+       XML_EVENT_MAILBOX,
+       XML_EVENT_EMAIL_ADDRESS,
+};
+
+struct ewscal_pvt {
+       AST_DECLARE_STRING_FIELDS(
+               AST_STRING_FIELD(url);
+               AST_STRING_FIELD(user);
+               AST_STRING_FIELD(secret);
+       );
+       struct ast_calendar *owner;
+       ne_uri uri;
+       ne_session *session;
+       struct ao2_container *events;
+};
+
+static void ewscal_destructor(void *obj)
+{
+       struct ewscal_pvt *pvt = obj;
+
+       ast_debug(1, "Destroying pvt for Exchange Web Service calendar %s\n", "pvt->owner->name");
+       if (pvt->session) {
+               ne_session_destroy(pvt->session);
+       }
+       ast_string_field_free_memory(pvt);
+
+       ao2_callback(pvt->events, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, NULL, NULL);
+
+       ao2_ref(pvt->events, -1);
+}
+
+static void *unref_ewscal(void *obj)
+{
+       struct ewscal_pvt *pvt = obj;
+
+       ast_debug(5, "EWS: unref_ewscal()\n");
+       ao2_ref(pvt, -1);
+       return NULL;
+}
+
+static int auth_credentials(void *userdata, const char *realm, int attempts, char *username, char *secret)
+{
+       struct ewscal_pvt *pvt = userdata;
+
+       if (attempts > 1) {
+               ast_log(LOG_WARNING, "Invalid username or password for Exchange Web Service calendar '%s'\n", pvt->owner->name);
+               return -1;
+       }
+
+       ne_strnzcpy(username, pvt->user, NE_ABUFSIZ);
+       ne_strnzcpy(secret, pvt->secret, NE_ABUFSIZ);
+
+       return 0;
+}
+
+static int ssl_verify(void *userdata, int failures, const ne_ssl_certificate *cert)
+{
+       struct ewscal_pvt *pvt = userdata;
+       if (failures & NE_SSL_UNTRUSTED) {
+               ast_log(LOG_WARNING, "Untrusted SSL certificate for calendar %s!\n", pvt->owner->name);
+               return 0;
+       }
+       return 1;       /* NE_SSL_NOTYETVALID, NE_SSL_EXPIRED, NE_SSL_IDMISMATCH */
+}
+
+static time_t mstime_to_time_t(char *mstime)
+{
+       struct ast_tm tm;
+       struct timeval tv;
+
+       if (ast_strptime(mstime, "%FT%TZ", &tm)) {
+               tv = ast_mktime(&tm, "UTC");
+               return tv.tv_sec;
+       }
+       return 0;
+}
+
+static int startelm(void *userdata, int parent, const char *nspace, const char *name, const char **atts)
+{
+       struct xml_context *ctx = userdata;
+
+       ast_debug(3, "EWS: XML: Start: %s\n", name);
+       if (ctx->op == XML_OP_CREATE) {
+               return NE_XML_DECLINE;
+       }
+
+       /* Nodes needed for traversing until CalendarItem is found */
+       if (!strcmp(name, "Envelope") ||
+               !strcmp(name, "Body") ||
+               !strcmp(name, "FindItemResponse") ||
+               !strcmp(name, "GetItemResponse") ||
+               !strcmp(name, "CreateItemResponse") ||
+               !strcmp(name, "ResponseMessages") ||
+               !strcmp(name, "FindItemResponseMessage") || !strcmp(name, "GetItemResponseMessage") ||
+               !strcmp(name, "Items")
+       ) {
+               return 1;
+       } else if (!strcmp(name, "RootFolder")) {
+               /* Get number of events */
+               int items;
+
+               ast_debug(3, "EWS: XML: <RootFolder>\n");
+               if (sscanf(ne_xml_get_attr(ctx->parser, atts, NULL, "TotalItemsInView"), "%d", &items) != 1) {
+                       /* Couldn't read enything */
+                       ne_xml_set_error(ctx->parser, "Could't read number of events.");
+                       return NE_XML_ABORT;
+               }
+
+               ast_debug(3, "EWS: %d calendar items to load\n", items);
+               if (items < 1) {
+                       /* Stop processing XML if there are no events */
+                       return NE_XML_DECLINE;
+               }
+               return 1;
+       } else if (!strcmp(name, "CalendarItem")) {
+               /* Event start */
+               ast_debug(3, "EWS: XML: <CalendarItem>\n");
+               if (!(ctx->pvt && ctx->pvt->owner)) {
+                       ast_log(LOG_ERROR, "Require a private structure with an owner\n");
+                       return NE_XML_ABORT;
+               }
+
+               ctx->event = ast_calendar_event_alloc(ctx->pvt->owner);
+               if (!ctx->event) {
+                       ast_log(LOG_ERROR, "Could not allocate an event!\n");
+                       return NE_XML_ABORT;
+               }
+
+               ctx->cdata = ast_str_create(64);
+               if (!ctx->cdata) {
+                       ast_log(LOG_ERROR, "Could not allocate CDATA!\n");
+                       return NE_XML_ABORT;
+               }
+
+               return 1;
+       } else if (!strcmp(name, "ItemId")) {
+               /* Event UID */
+               if (ctx->op == XML_OP_FIND) {
+                       struct calendar_id *id;
+                       if (!(id = ast_calloc(1, sizeof(id)))) {
+                               return NE_XML_ABORT;
+                       }
+                       if (!(id->id = ast_str_create(256))) {
+                               ast_free(id);
+                               return NE_XML_ABORT;
+                       }
+                       ast_str_set(&id->id, 0, "%s", ne_xml_get_attr(ctx->parser, atts, NULL, "Id"));
+                       AST_LIST_INSERT_TAIL(&ctx->ids, id, next);
+                       ast_debug(3, "EWS_FIND: XML: UID: %s\n", ast_str_buffer(id->id));
+               } else {
+                       ast_debug(3, "EWS_GET: XML: UID: %s\n", ne_xml_get_attr(ctx->parser, atts, NULL, "Id"));
+                       ast_string_field_set(ctx->event, uid, ne_xml_get_attr(ctx->parser, atts, NULL, "Id"));
+               }
+               return XML_EVENT_NAME;
+       } else if (!strcmp(name, "Subject")) {
+               /* Event name */
+               if (!ctx->cdata) {
+                       return NE_XML_ABORT;
+               }
+               ast_str_reset(ctx->cdata);
+               return XML_EVENT_NAME;
+       } else if (!strcmp(name, "Start")) {
+               /* Event start time */
+               return XML_EVENT_START;
+       } else if (!strcmp(name, "End")) {
+               /* Event end time */
+               return XML_EVENT_END;
+       } else if (!strcmp(name, "LegacyFreeBusyStatus")) {
+               /* Event busy state */
+               return XML_EVENT_BUSY;
+       } else if (!strcmp(name, "Organizer") ||
+                       (parent == XML_EVENT_ORGANIZER && (!strcmp(name, "Mailbox") ||
+                       !strcmp(name, "Name")))) {
+               /* Event organizer */
+               if (!ctx->cdata) {
+                       return NE_XML_ABORT;
+               }
+               ast_str_reset(ctx->cdata);
+               return XML_EVENT_ORGANIZER;
+       } else if (!strcmp(name, "Location")) {
+               /* Event location */
+               if (!ctx->cdata) {
+                       return NE_XML_ABORT;
+               }
+               ast_str_reset(ctx->cdata);
+               return XML_EVENT_LOCATION;
+       } else if (!strcmp(name, "RequiredAttendees") || !strcmp(name, "OptionalAttendees")) {
+               return XML_EVENT_ATTENDEE_LIST;
+       } else if (!strcmp(name, "Attendee") && parent == XML_EVENT_ATTENDEE_LIST) {
+               return XML_EVENT_ATTENDEE;
+       } else if (!strcmp(name, "Mailbox") && parent == XML_EVENT_ATTENDEE) {
+               return XML_EVENT_MAILBOX;
+       } else if (!strcmp(name, "EmailAddress") && parent == XML_EVENT_MAILBOX) {
+               if (!ctx->cdata) {
+                       return NE_XML_ABORT;
+               }
+               ast_str_reset(ctx->cdata);
+               return XML_EVENT_EMAIL_ADDRESS;
+       }
+
+       return NE_XML_DECLINE;
+}
+
+static int cdata(void *userdata, int state, const char *cdata, size_t len)
+{
+       struct xml_context *ctx = userdata;
+       char data[len + 1];
+
+       /* !!! DON'T USE AST_STRING_FIELD FUNCTIONS HERE, JUST COLLECT CTX->CDATA !!! */
+       if (state < XML_EVENT_NAME || ctx->op == XML_OP_CREATE) {
+               return 0;
+       }
+
+       if (!ctx->event) {
+               ast_log(LOG_ERROR, "Parsing event data, but event object does not exist!\n");
+               return 1;
+       }
+
+       if (!ctx->cdata) {
+               ast_log(LOG_ERROR, "String for storing CDATA is unitialized!\n");
+               return 1;
+       }
+
+       ast_copy_string(data, cdata, len + 1);
+
+       switch (state) {
+       case XML_EVENT_START:
+               ctx->event->start = mstime_to_time_t(data);
+               break;
+       case XML_EVENT_END:
+               ctx->event->end = mstime_to_time_t(data);
+               break;
+       case XML_EVENT_BUSY:
+               if (!strcmp(data, "Busy") || !strcmp(data, "OOF")) {
+                       ast_debug(3, "EWS: XML: Busy: yes\n");
+                       ctx->event->busy_state = AST_CALENDAR_BS_BUSY;
+               }
+               else if (!strcmp(data, "Tentative")) {
+                       ast_debug(3, "EWS: XML: Busy: tentative\n");
+                       ctx->event->busy_state = AST_CALENDAR_BS_BUSY_TENTATIVE;
+               }
+               else {
+                       ast_debug(3, "EWS: XML: Busy: no\n");
+                       ctx->event->busy_state = AST_CALENDAR_BS_FREE;
+               }
+               break;
+       default:
+               ast_str_append(&ctx->cdata, 0, "%s", data);
+       }
+
+       ast_debug(5, "EWS: XML: CDATA: %s\n", ast_str_buffer(ctx->cdata));
+
+       return 0;
+}
+
+static int endelm(void *userdata, int state, const char *nspace, const char *name)
+{
+       struct xml_context *ctx = userdata;
+
+       ast_debug(5, "EWS: XML: End:   %s\n", name);
+       if (ctx->op == XML_OP_FIND || ctx->op == XML_OP_CREATE) {
+               return NE_XML_DECLINE;
+       }
+
+       if (!strcmp(name, "Subject")) {
+               /* Event name end*/
+               ast_string_field_set(ctx->event, summary, ast_str_buffer(ctx->cdata));
+               ast_debug(3, "EWS: XML: Summary: %s\n", ctx->event->summary);
+               ast_str_reset(ctx->cdata);
+       } else if (!strcmp(name, "Organizer")) {
+               /* Event organizer end */
+               ast_string_field_set(ctx->event, organizer, ast_str_buffer(ctx->cdata));
+               ast_debug(3, "EWS: XML: Organizer: %s\n", ctx->event->organizer);
+               ast_str_reset(ctx->cdata);
+       } else if (!strcmp(name, "Location")) {
+               /* Event location end */
+               ast_string_field_set(ctx->event, location, ast_str_buffer(ctx->cdata));
+               ast_debug(3, "EWS: XML: Location: %s\n", ctx->event->location);
+               ast_str_reset(ctx->cdata);
+       } else if (state == XML_EVENT_EMAIL_ADDRESS) {
+               struct ast_calendar_attendee *attendee;
+
+               if (!(attendee = ast_calloc(1, sizeof(*attendee)))) {
+                       ctx->event = ast_calendar_unref_event(ctx->event);
+                       return  1;
+               }
+
+               if (ast_str_strlen(ctx->cdata)) {
+                       attendee->data = ast_strdup(ast_str_buffer(ctx->cdata));
+                       AST_LIST_INSERT_TAIL(&ctx->event->attendees, attendee, next);
+               }
+               ast_debug(3, "EWS: XML: attendee address '%s'\n", ast_str_buffer(ctx->cdata));
+               ast_str_reset(ctx->cdata);
+       } else if (!strcmp(name, "CalendarItem")) {
+               /* Event end */
+               ast_debug(3, "EWS: XML: </CalendarItem>\n");
+               ast_free(ctx->cdata);
+               if (ctx->event) {
+                       ao2_link(ctx->pvt->events, ctx->event);
+                       ctx->event = ast_calendar_unref_event(ctx->event);
+               } else {
+                       ast_log(LOG_ERROR, "Event data ended in XML, but event object does not exist!\n");
+                       return 1;
+               }
+       } else if (!strcmp(name, "Envelope")) {
+               /* Events end */
+               ast_debug(3, "EWS: XML: All events has been parsed, merging…\n");
+               ast_calendar_merge_events(ctx->pvt->owner, ctx->pvt->events);
+       }
+
+       return 0;
+}
+
+static const char *mstime(time_t t, char *buf, size_t buflen)
+{
+       struct timeval tv = {
+               .tv_sec = t,
+       };
+       struct ast_tm tm;
+
+       ast_localtime(&tv, &tm, "utc");
+       ast_strftime(buf, buflen, "%FT%TZ", &tm);
+
+       return S_OR(buf, "");
+}
+
+static const char *msstatus(enum ast_calendar_busy_state state)
+{
+       switch (state) {
+       case AST_CALENDAR_BS_BUSY_TENTATIVE:
+               return "Tentative";
+       case AST_CALENDAR_BS_BUSY:
+               return "Busy";
+       case AST_CALENDAR_BS_FREE:
+               return "Free";
+       default:
+               return "";
+       }
+}
+
+static const char *get_soap_action(enum xml_op op)
+{
+       switch (op) {
+       case XML_OP_FIND:
+               return "\"http://schemas.microsoft.com/exchange/services/2006/messages/FindItem\"";
+       case XML_OP_GET:
+               return "\"http://schemas.microsoft.com/exchange/services/2006/messages/GetItem\"";
+       case XML_OP_CREATE:
+               return "\"http://schemas.microsoft.com/exchange/services/2006/messages/CreateItem\"";
+       }
+
+       return "";
+}
+
+static int send_ews_request_and_parse(struct ast_str *request, struct xml_context *ctx)
+{
+       int ret;
+       ne_request *req;
+       ne_xml_parser *parser;
+
+       ast_debug(3, "EWS: HTTP request...\n");
+       if (!(ctx && ctx->pvt)) {
+               ast_log(LOG_ERROR, "There is no private!\n");
+               return -1;
+       }
+
+       if (!ast_str_strlen(request)) {
+               ast_log(LOG_ERROR, "No request to send!\n");
+               return -1;
+       }
+
+       ast_debug(3, "%s\n", ast_str_buffer(request));
+
+       /* Prepare HTTP POST request */
+       req = ne_request_create(ctx->pvt->session, "POST", ctx->pvt->uri.path);
+       ne_set_request_flag(req, NE_REQFLAG_IDEMPOTENT, 0);
+
+       /* Set headers--should be application/soap+xml, but MS… :/ */
+       ne_add_request_header(req, "Content-Type", "text/xml; charset=utf-8");
+       ne_add_request_header(req, "SOAPAction", get_soap_action(ctx->op));
+
+       /* Set body to SOAP request */
+       ne_set_request_body_buffer(req, ast_str_buffer(request), ast_str_strlen(request));
+
+       /* Prepare XML parser */
+       parser = ne_xml_create();
+       ctx->parser = parser;
+       ne_xml_push_handler(parser, startelm, cdata, endelm, ctx);      /* Callbacks */
+
+       /* Dispatch request and parse response as XML */
+       ret = ne_xml_dispatch_request(req, parser);
+       if (ret != NE_OK) { /* Error handling */
+               ast_log(LOG_WARNING, "Unable to communicate with Exchange Web Service at '%s': %s\n", ctx->pvt->url, ne_get_error(ctx->pvt->session));
+               ne_request_destroy(req);
+               ast_free(request);
+               ne_xml_destroy(parser);
+               return -1;
+       }
+
+       /* Cleanup */
+       ne_request_destroy(req);
+       ne_xml_destroy(parser);
+
+       return 0;
+}
+
+static int ewscal_write_event(struct ast_calendar_event *event)
+{
+       struct ast_str *request;
+       struct ewscal_pvt *pvt = event->owner->tech_pvt;
+       char start[21], end[21];
+       struct xml_context ctx = {
+               .op = XML_OP_CREATE,
+               .pvt = pvt,
+       };
+       int ret;
+
+       if (!pvt) {
+               return -1;
+       }
+
+       if (!(request = ast_str_create(1024))) {
+               return -1;
+       }
+
+       ast_str_set(&request, 0,
+               "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
+                       "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
+                       "xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
+                       "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
+                       "<soap:Body>"
+                       "<CreateItem xmlns=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
+                               "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" "
+                               "SendMeetingInvitations=\"SendToNone\" >"
+                               "<SavedItemFolderId>"
+                                       "<t:DistinguishedFolderId Id=\"calendar\"/>"
+                               "</SavedItemFolderId>"
+                               "<Items>"
+                                       "<t:CalendarItem xmlns=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
+                                               "<Subject>%s</Subject>"
+                                               "<Body BodyType=\"Text\">%s</Body>"
+                                               "<ReminderIsSet>false</ReminderIsSet>"
+                                               "<Start>%s</Start>"
+                                               "<End>%s</End>"
+                                               "<IsAllDayEvent>false</IsAllDayEvent>"
+                                               "<LegacyFreeBusyStatus>%s</LegacyFreeBusyStatus>"
+                                               "<Location>%s</Location>"
+                                       "</t:CalendarItem>"
+                               "</Items>"
+                       "</CreateItem>"
+               "</soap:Body>"
+               "</soap:Envelope>",
+               event->summary,
+               event->description,
+               mstime(event->start, start, sizeof(start)),
+               mstime(event->end, end, sizeof(end)),
+               msstatus(event->busy_state),
+               event->location
+       );
+
+       ret = send_ews_request_and_parse(request, &ctx);
+
+       ast_free(request);
+
+       return ret;
+}
+
+static struct calendar_id *get_ewscal_ids_for(struct ewscal_pvt *pvt)
+{
+       char start[21], end[21];
+       struct ast_tm tm;
+       struct timeval tv;
+       struct ast_str *request;
+       struct xml_context ctx = {
+               .op = XML_OP_FIND,
+               .pvt = pvt,
+       };
+
+       ast_debug(5, "EWS: get_ewscal_ids_for()\n");
+
+       if (!pvt) {
+               ast_log(LOG_ERROR, "There is no private!\n");
+               return NULL;
+       }
+
+       /* Prepare timeframe strings */
+       tv = ast_tvnow();
+       ast_localtime(&tv, &tm, "UTC");
+       ast_strftime(start, sizeof(start), "%FT%TZ", &tm);
+       tv.tv_sec += 60 * pvt->owner->timeframe;
+       ast_localtime(&tv, &tm, "UTC");
+       ast_strftime(end, sizeof(end), "%FT%TZ", &tm);
+
+       /* Prepare SOAP request */
+       if (!(request = ast_str_create(512))) {
+               return NULL;
+       }
+
+       ast_str_set(&request, 0,
+               "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" "
+               "xmlns:ns1=\"http://schemas.microsoft.com/exchange/services/2006/types\" "
+               "xmlns:ns2=\"http://schemas.microsoft.com/exchange/services/2006/messages\">"
+                       "<SOAP-ENV:Body>"
+                               "<ns2:FindItem Traversal=\"Shallow\">"
+                                       "<ns2:ItemShape>"
+                                               "<ns1:BaseShape>IdOnly</ns1:BaseShape>"
+                                       "</ns2:ItemShape>"
+                                       "<ns2:CalendarView StartDate=\"%s\" EndDate=\"%s\"/>"   /* Timeframe */
+                                       "<ns2:ParentFolderIds>"
+                                               "<ns1:DistinguishedFolderId Id=\"calendar\"/>"
+                                       "</ns2:ParentFolderIds>"
+                               "</ns2:FindItem>"
+                       "</SOAP-ENV:Body>"
+               "</SOAP-ENV:Envelope>",
+               start, end      /* Timeframe */
+       );
+
+       AST_LIST_HEAD_INIT_NOLOCK(&ctx.ids);
+
+       /* Dispatch request and parse response as XML */
+       if (send_ews_request_and_parse(request, &ctx)) {
+               ast_free(request);
+               return NULL;
+       }
+
+       /* Cleanup */
+       ast_free(request);
+
+       return AST_LIST_FIRST(&ctx.ids);
+}
+
+static int parse_ewscal_id(struct ewscal_pvt *pvt, const char *id) {
+       struct ast_str *request;
+       struct xml_context ctx = {
+               .pvt = pvt,
+               .op = XML_OP_GET,
+       };
+
+       if (!(request = ast_str_create(512))) {
+               return -1;
+       }
+
+       ast_str_set(&request, 0,
+               "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+               "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
+               "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
+               "<soap:Body>"
+                       "<GetItem xmlns=\"http://schemas.microsoft.com/exchange/services/2006/messages\">"
+                               "<ItemShape>"
+                                       "<t:BaseShape>AllProperties</t:BaseShape>"
+                               "</ItemShape>"
+                               "<ItemIds>"
+                                       "<t:ItemId Id=\"%s\"/>"
+                               "</ItemIds>"
+                       "</GetItem>"
+               "</soap:Body>"
+               "</soap:Envelope>", id
+       );
+
+       if (send_ews_request_and_parse(request, &ctx)) {
+               ast_free(request);
+               return -1;
+       }
+
+       ast_free(request);
+
+       return 0;
+}
+
+static int update_ewscal(struct ewscal_pvt *pvt)
+{
+       struct calendar_id *id_head;
+       struct calendar_id *iter;
+
+       if (!(id_head = get_ewscal_ids_for(pvt))) {
+               return 0;
+       }
+
+       for (iter = id_head; iter; iter = AST_LIST_NEXT(iter, next)) {
+               parse_ewscal_id(pvt, ast_str_buffer(iter->id));
+               ast_free(iter->id);
+               ast_free(iter);
+       }
+
+       return 0;
+}
+
+static void *ewscal_load_calendar(void *void_data)
+{
+       struct ewscal_pvt *pvt;
+       const struct ast_config *cfg;
+       struct ast_variable *v;
+       struct ast_calendar *cal = void_data;
+       ast_mutex_t refreshlock;
+
+       ast_debug(5, "EWS: ewscal_load_calendar()\n");
+
+       if (!(cal && (cfg = ast_calendar_config_acquire()))) {
+               ast_log(LOG_ERROR, "You must enable calendar support for res_ewscal to load\n");
+               return NULL;
+       }
+
+       if (ao2_trylock(cal)) {
+               if (cal->unloading) {
+                       ast_log(LOG_WARNING, "Unloading module, load_calendar cancelled.\n");
+               } else {
+                       ast_log(LOG_WARNING, "Could not lock calendar, aborting!\n");
+               }
+               ast_calendar_config_release();
+               return NULL;
+       }
+
+       if (!(pvt = ao2_alloc(sizeof(*pvt), ewscal_destructor))) {
+               ast_log(LOG_ERROR, "Could not allocate ewscal_pvt structure for calendar: %s\n", cal->name);
+               ast_calendar_config_release();
+               return NULL;
+       }
+
+       pvt->owner = cal;
+
+       if (!(pvt->events = ast_calendar_event_container_alloc())) {
+               ast_log(LOG_ERROR, "Could not allocate space for fetching events for calendar: %s\n", cal->name);
+               pvt = unref_ewscal(pvt);
+               ao2_unlock(cal);
+               ast_calendar_config_release();
+               return NULL;
+       }
+
+       if (ast_string_field_init(pvt, 32)) {
+               ast_log(LOG_ERROR, "Couldn't allocate string field space for calendar: %s\n", cal->name);
+               pvt = unref_ewscal(pvt);
+               ao2_unlock(cal);
+               ast_calendar_config_release();
+               return NULL;
+       }
+
+       for (v = ast_variable_browse(cfg, cal->name); v; v = v->next) {
+               if (!strcasecmp(v->name, "url")) {
+                       ast_string_field_set(pvt, url, v->value);
+               } else if (!strcasecmp(v->name, "user")) {
+                       ast_string_field_set(pvt, user, v->value);
+               } else if (!strcasecmp(v->name, "secret")) {
+                       ast_string_field_set(pvt, secret, v->value);
+               }
+       }
+
+       ast_calendar_config_release();
+
+       if (ast_strlen_zero(pvt->url)) {
+               ast_log(LOG_WARNING, "No URL was specified for Exchange Web Service calendar '%s' - skipping.\n", cal->name);
+               pvt = unref_ewscal(pvt);
+               ao2_unlock(cal);
+               return NULL;
+       }
+
+       if (ne_uri_parse(pvt->url, &pvt->uri) || pvt->uri.host == NULL || pvt->uri.path == NULL) {
+               ast_log(LOG_WARNING, "Could not parse url '%s' for Exchange Web Service calendar '%s' - skipping.\n", pvt->url, cal->name);
+               pvt = unref_ewscal(pvt);
+               ao2_unlock(cal);
+               return NULL;
+       }
+
+       if (pvt->uri.scheme == NULL) {
+               pvt->uri.scheme = "http";
+       }
+
+       if (pvt->uri.port == 0) {
+               pvt->uri.port = ne_uri_defaultport(pvt->uri.scheme);
+       }
+
+       ast_debug(3, "ne_uri.scheme     = %s\n", pvt->uri.scheme);
+       ast_debug(3, "ne_uri.host       = %s\n", pvt->uri.host);
+       ast_debug(3, "ne_uri.port       = %u\n", pvt->uri.port);
+       ast_debug(3, "ne_uri.path       = %s\n", pvt->uri.path);
+       ast_debug(3, "user              = %s\n", pvt->user);
+       ast_debug(3, "secret            = %s\n", pvt->secret);
+
+       pvt->session = ne_session_create(pvt->uri.scheme, pvt->uri.host, pvt->uri.port);
+       ne_set_server_auth(pvt->session, auth_credentials, pvt);
+       ne_set_useragent(pvt->session, "Asterisk");
+
+       if (!strcasecmp(pvt->uri.scheme, "https")) {
+               ne_ssl_trust_default_ca(pvt->session);
+               ne_ssl_set_verify(pvt->session, ssl_verify, pvt);
+       }
+
+       cal->tech_pvt = pvt;
+
+       ast_mutex_init(&refreshlock);
+
+       /* Load it the first time */
+       update_ewscal(pvt);
+
+       ao2_unlock(cal);
+
+       /* The only writing from another thread will be if unload is true */
+       for (;;) {
+               struct timeval tv = ast_tvnow();
+               struct timespec ts = {0,};
+
+               ts.tv_sec = tv.tv_sec + (60 * pvt->owner->refresh);
+
+               ast_mutex_lock(&refreshlock);
+               while (!pvt->owner->unloading) {
+                       if (ast_cond_timedwait(&pvt->owner->unload, &refreshlock, &ts) == ETIMEDOUT) {
+                               break;
+                       }
+               }
+               ast_mutex_unlock(&refreshlock);
+
+               if (pvt->owner->unloading) {
+                       ast_debug(10, "Skipping refresh since we got a shutdown signal\n");
+                       return NULL;
+               }
+
+               ast_debug(10, "Refreshing after %d minute timeout\n", pvt->owner->refresh);
+
+               update_ewscal(pvt);
+       }
+
+       return NULL;
+}
+
+static int load_module(void)
+{
+       /* Actualy, 0.29.1 is required (because of NTLM authentication), but this
+        * function does not support matching patch version. */
+       if (ne_version_match(0, 29)) {
+               ast_log(LOG_ERROR, "Exchange Web Service calendar module require neon >= 0.29.1, but %s is installed.\n", ne_version_string());
+               return AST_MODULE_LOAD_DECLINE;
+       }
+
+       if (ast_calendar_register(&ewscal_tech) && (ne_sock_init() == 0)) {
+               return AST_MODULE_LOAD_DECLINE;
+       }
+
+       return AST_MODULE_LOAD_SUCCESS;
+}
+
+static int unload_module(void)
+{
+       ne_sock_exit();
+       ast_calendar_unregister(&ewscal_tech);
+
+       return 0;
+}
+
+AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk MS Exchange Web Service Calendar Integration",
+       .load = load_module,
+       .unload = unload_module,
+);