Calendaring support for Exchange Server 2007+ via EWS
[asterisk/asterisk.git] / res / res_calendar_ews.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2008 - 2009, Digium, Inc.
5  *
6  * Jan Kalab <pitlicek@gmail.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  * \brief Resource for handling MS Exchange Web Service calendars
21  */
22
23 /*** MODULEINFO
24         <depend>neon</depend>
25 ***/
26 #include "asterisk.h"
27
28 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
29
30 #include <neon/ne_request.h>
31 #include <neon/ne_session.h>
32 #include <neon/ne_uri.h>
33 #include <neon/ne_socket.h>
34 #include <neon/ne_auth.h>
35 #include <neon/ne_xml.h>
36 #include <neon/ne_xmlreq.h>
37 #include <neon/ne_utils.h>
38
39 #include "asterisk/module.h"
40 #include "asterisk/calendar.h"
41 #include "asterisk/lock.h"
42 #include "asterisk/config.h"
43 #include "asterisk/astobj2.h"
44
45 static void *ewscal_load_calendar(void *data);
46 static void *unref_ewscal(void *obj);
47 static int ewscal_write_event(struct ast_calendar_event *event);
48
49 static struct ast_calendar_tech ewscal_tech = {
50         .type = "ews",
51         .description = "MS Exchange Web Service calendars",
52         .module = AST_MODULE,
53         .load_calendar = ewscal_load_calendar,
54         .unref_calendar = unref_ewscal,
55         .write_event = ewscal_write_event,
56 };
57
58 enum xml_op {
59         XML_OP_FIND = 100,
60         XML_OP_GET,
61         XML_OP_CREATE,
62 };
63
64 struct calendar_id {
65         struct ast_str *id;
66         AST_LIST_ENTRY(calendar_id) next;
67 };
68
69 struct xml_context {
70         ne_xml_parser *parser;
71         struct ast_str *cdata;
72         struct ast_calendar_event *event;
73         enum xml_op op;
74         struct ewscal_pvt *pvt;
75         AST_LIST_HEAD_NOLOCK(ids, calendar_id) ids;
76 };
77
78 /* Important states of XML parsing */
79 enum {
80         XML_EVENT_NAME = 10,
81         XML_EVENT_START,
82         XML_EVENT_END,
83         XML_EVENT_BUSY,
84         XML_EVENT_ORGANIZER,
85         XML_EVENT_LOCATION,
86         XML_EVENT_ATTENDEE_LIST,
87         XML_EVENT_ATTENDEE,
88         XML_EVENT_MAILBOX,
89         XML_EVENT_EMAIL_ADDRESS,
90 };
91
92 struct ewscal_pvt {
93         AST_DECLARE_STRING_FIELDS(
94                 AST_STRING_FIELD(url);
95                 AST_STRING_FIELD(user);
96                 AST_STRING_FIELD(secret);
97         );
98         struct ast_calendar *owner;
99         ne_uri uri;
100         ne_session *session;
101         struct ao2_container *events;
102 };
103
104 static void ewscal_destructor(void *obj)
105 {
106         struct ewscal_pvt *pvt = obj;
107
108         ast_debug(1, "Destroying pvt for Exchange Web Service calendar %s\n", "pvt->owner->name");
109         if (pvt->session) {
110                 ne_session_destroy(pvt->session);
111         }
112         ast_string_field_free_memory(pvt);
113
114         ao2_callback(pvt->events, OBJ_UNLINK | OBJ_NODATA | OBJ_MULTIPLE, NULL, NULL);
115
116         ao2_ref(pvt->events, -1);
117 }
118
119 static void *unref_ewscal(void *obj)
120 {
121         struct ewscal_pvt *pvt = obj;
122
123         ast_debug(5, "EWS: unref_ewscal()\n");
124         ao2_ref(pvt, -1);
125         return NULL;
126 }
127
128 static int auth_credentials(void *userdata, const char *realm, int attempts, char *username, char *secret)
129 {
130         struct ewscal_pvt *pvt = userdata;
131
132         if (attempts > 1) {
133                 ast_log(LOG_WARNING, "Invalid username or password for Exchange Web Service calendar '%s'\n", pvt->owner->name);
134                 return -1;
135         }
136
137         ne_strnzcpy(username, pvt->user, NE_ABUFSIZ);
138         ne_strnzcpy(secret, pvt->secret, NE_ABUFSIZ);
139
140         return 0;
141 }
142
143 static int ssl_verify(void *userdata, int failures, const ne_ssl_certificate *cert)
144 {
145         struct ewscal_pvt *pvt = userdata;
146         if (failures & NE_SSL_UNTRUSTED) {
147                 ast_log(LOG_WARNING, "Untrusted SSL certificate for calendar %s!\n", pvt->owner->name);
148                 return 0;
149         }
150         return 1;       /* NE_SSL_NOTYETVALID, NE_SSL_EXPIRED, NE_SSL_IDMISMATCH */
151 }
152
153 static time_t mstime_to_time_t(char *mstime)
154 {
155         struct ast_tm tm;
156         struct timeval tv;
157
158         if (ast_strptime(mstime, "%FT%TZ", &tm)) {
159                 tv = ast_mktime(&tm, "UTC");
160                 return tv.tv_sec;
161         }
162         return 0;
163 }
164
165 static int startelm(void *userdata, int parent, const char *nspace, const char *name, const char **atts)
166 {
167         struct xml_context *ctx = userdata;
168
169         ast_debug(3, "EWS: XML: Start: %s\n", name);
170         if (ctx->op == XML_OP_CREATE) {
171                 return NE_XML_DECLINE;
172         }
173
174         /* Nodes needed for traversing until CalendarItem is found */
175         if (!strcmp(name, "Envelope") ||
176                 !strcmp(name, "Body") ||
177                 !strcmp(name, "FindItemResponse") ||
178                 !strcmp(name, "GetItemResponse") ||
179                 !strcmp(name, "CreateItemResponse") ||
180                 !strcmp(name, "ResponseMessages") ||
181                 !strcmp(name, "FindItemResponseMessage") || !strcmp(name, "GetItemResponseMessage") ||
182                 !strcmp(name, "Items")
183         ) {
184                 return 1;
185         } else if (!strcmp(name, "RootFolder")) {
186                 /* Get number of events */
187                 int items;
188
189                 ast_debug(3, "EWS: XML: <RootFolder>\n");
190                 if (sscanf(ne_xml_get_attr(ctx->parser, atts, NULL, "TotalItemsInView"), "%d", &items) != 1) {
191                         /* Couldn't read enything */
192                         ne_xml_set_error(ctx->parser, "Could't read number of events.");
193                         return NE_XML_ABORT;
194                 }
195
196                 ast_debug(3, "EWS: %d calendar items to load\n", items);
197                 if (items < 1) {
198                         /* Stop processing XML if there are no events */
199                         return NE_XML_DECLINE;
200                 }
201                 return 1;
202         } else if (!strcmp(name, "CalendarItem")) {
203                 /* Event start */
204                 ast_debug(3, "EWS: XML: <CalendarItem>\n");
205                 if (!(ctx->pvt && ctx->pvt->owner)) {
206                         ast_log(LOG_ERROR, "Require a private structure with an owner\n");
207                         return NE_XML_ABORT;
208                 }
209
210                 ctx->event = ast_calendar_event_alloc(ctx->pvt->owner);
211                 if (!ctx->event) {
212                         ast_log(LOG_ERROR, "Could not allocate an event!\n");
213                         return NE_XML_ABORT;
214                 }
215
216                 ctx->cdata = ast_str_create(64);
217                 if (!ctx->cdata) {
218                         ast_log(LOG_ERROR, "Could not allocate CDATA!\n");
219                         return NE_XML_ABORT;
220                 }
221
222                 return 1;
223         } else if (!strcmp(name, "ItemId")) {
224                 /* Event UID */
225                 if (ctx->op == XML_OP_FIND) {
226                         struct calendar_id *id;
227                         if (!(id = ast_calloc(1, sizeof(id)))) {
228                                 return NE_XML_ABORT;
229                         }
230                         if (!(id->id = ast_str_create(256))) {
231                                 ast_free(id);
232                                 return NE_XML_ABORT;
233                         }
234                         ast_str_set(&id->id, 0, "%s", ne_xml_get_attr(ctx->parser, atts, NULL, "Id"));
235                         AST_LIST_INSERT_TAIL(&ctx->ids, id, next);
236                         ast_debug(3, "EWS_FIND: XML: UID: %s\n", ast_str_buffer(id->id));
237                 } else {
238                         ast_debug(3, "EWS_GET: XML: UID: %s\n", ne_xml_get_attr(ctx->parser, atts, NULL, "Id"));
239                         ast_string_field_set(ctx->event, uid, ne_xml_get_attr(ctx->parser, atts, NULL, "Id"));
240                 }
241                 return XML_EVENT_NAME;
242         } else if (!strcmp(name, "Subject")) {
243                 /* Event name */
244                 if (!ctx->cdata) {
245                         return NE_XML_ABORT;
246                 }
247                 ast_str_reset(ctx->cdata);
248                 return XML_EVENT_NAME;
249         } else if (!strcmp(name, "Start")) {
250                 /* Event start time */
251                 return XML_EVENT_START;
252         } else if (!strcmp(name, "End")) {
253                 /* Event end time */
254                 return XML_EVENT_END;
255         } else if (!strcmp(name, "LegacyFreeBusyStatus")) {
256                 /* Event busy state */
257                 return XML_EVENT_BUSY;
258         } else if (!strcmp(name, "Organizer") ||
259                         (parent == XML_EVENT_ORGANIZER && (!strcmp(name, "Mailbox") ||
260                         !strcmp(name, "Name")))) {
261                 /* Event organizer */
262                 if (!ctx->cdata) {
263                         return NE_XML_ABORT;
264                 }
265                 ast_str_reset(ctx->cdata);
266                 return XML_EVENT_ORGANIZER;
267         } else if (!strcmp(name, "Location")) {
268                 /* Event location */
269                 if (!ctx->cdata) {
270                         return NE_XML_ABORT;
271                 }
272                 ast_str_reset(ctx->cdata);
273                 return XML_EVENT_LOCATION;
274         } else if (!strcmp(name, "RequiredAttendees") || !strcmp(name, "OptionalAttendees")) {
275                 return XML_EVENT_ATTENDEE_LIST;
276         } else if (!strcmp(name, "Attendee") && parent == XML_EVENT_ATTENDEE_LIST) {
277                 return XML_EVENT_ATTENDEE;
278         } else if (!strcmp(name, "Mailbox") && parent == XML_EVENT_ATTENDEE) {
279                 return XML_EVENT_MAILBOX;
280         } else if (!strcmp(name, "EmailAddress") && parent == XML_EVENT_MAILBOX) {
281                 if (!ctx->cdata) {
282                         return NE_XML_ABORT;
283                 }
284                 ast_str_reset(ctx->cdata);
285                 return XML_EVENT_EMAIL_ADDRESS;
286         }
287
288         return NE_XML_DECLINE;
289 }
290
291 static int cdata(void *userdata, int state, const char *cdata, size_t len)
292 {
293         struct xml_context *ctx = userdata;
294         char data[len + 1];
295
296         /* !!! DON'T USE AST_STRING_FIELD FUNCTIONS HERE, JUST COLLECT CTX->CDATA !!! */
297         if (state < XML_EVENT_NAME || ctx->op == XML_OP_CREATE) {
298                 return 0;
299         }
300
301         if (!ctx->event) {
302                 ast_log(LOG_ERROR, "Parsing event data, but event object does not exist!\n");
303                 return 1;
304         }
305
306         if (!ctx->cdata) {
307                 ast_log(LOG_ERROR, "String for storing CDATA is unitialized!\n");
308                 return 1;
309         }
310
311         ast_copy_string(data, cdata, len + 1);
312
313         switch (state) {
314         case XML_EVENT_START:
315                 ctx->event->start = mstime_to_time_t(data);
316                 break;
317         case XML_EVENT_END:
318                 ctx->event->end = mstime_to_time_t(data);
319                 break;
320         case XML_EVENT_BUSY:
321                 if (!strcmp(data, "Busy") || !strcmp(data, "OOF")) {
322                         ast_debug(3, "EWS: XML: Busy: yes\n");
323                         ctx->event->busy_state = AST_CALENDAR_BS_BUSY;
324                 }
325                 else if (!strcmp(data, "Tentative")) {
326                         ast_debug(3, "EWS: XML: Busy: tentative\n");
327                         ctx->event->busy_state = AST_CALENDAR_BS_BUSY_TENTATIVE;
328                 }
329                 else {
330                         ast_debug(3, "EWS: XML: Busy: no\n");
331                         ctx->event->busy_state = AST_CALENDAR_BS_FREE;
332                 }
333                 break;
334         default:
335                 ast_str_append(&ctx->cdata, 0, "%s", data);
336         }
337
338         ast_debug(5, "EWS: XML: CDATA: %s\n", ast_str_buffer(ctx->cdata));
339
340         return 0;
341 }
342
343 static int endelm(void *userdata, int state, const char *nspace, const char *name)
344 {
345         struct xml_context *ctx = userdata;
346
347         ast_debug(5, "EWS: XML: End:   %s\n", name);
348         if (ctx->op == XML_OP_FIND || ctx->op == XML_OP_CREATE) {
349                 return NE_XML_DECLINE;
350         }
351
352         if (!strcmp(name, "Subject")) {
353                 /* Event name end*/
354                 ast_string_field_set(ctx->event, summary, ast_str_buffer(ctx->cdata));
355                 ast_debug(3, "EWS: XML: Summary: %s\n", ctx->event->summary);
356                 ast_str_reset(ctx->cdata);
357         } else if (!strcmp(name, "Organizer")) {
358                 /* Event organizer end */
359                 ast_string_field_set(ctx->event, organizer, ast_str_buffer(ctx->cdata));
360                 ast_debug(3, "EWS: XML: Organizer: %s\n", ctx->event->organizer);
361                 ast_str_reset(ctx->cdata);
362         } else if (!strcmp(name, "Location")) {
363                 /* Event location end */
364                 ast_string_field_set(ctx->event, location, ast_str_buffer(ctx->cdata));
365                 ast_debug(3, "EWS: XML: Location: %s\n", ctx->event->location);
366                 ast_str_reset(ctx->cdata);
367         } else if (state == XML_EVENT_EMAIL_ADDRESS) {
368                 struct ast_calendar_attendee *attendee;
369
370                 if (!(attendee = ast_calloc(1, sizeof(*attendee)))) {
371                         ctx->event = ast_calendar_unref_event(ctx->event);
372                         return  1;
373                 }
374
375                 if (ast_str_strlen(ctx->cdata)) {
376                         attendee->data = ast_strdup(ast_str_buffer(ctx->cdata));
377                         AST_LIST_INSERT_TAIL(&ctx->event->attendees, attendee, next);
378                 }
379                 ast_debug(3, "EWS: XML: attendee address '%s'\n", ast_str_buffer(ctx->cdata));
380                 ast_str_reset(ctx->cdata);
381         } else if (!strcmp(name, "CalendarItem")) {
382                 /* Event end */
383                 ast_debug(3, "EWS: XML: </CalendarItem>\n");
384                 ast_free(ctx->cdata);
385                 if (ctx->event) {
386                         ao2_link(ctx->pvt->events, ctx->event);
387                         ctx->event = ast_calendar_unref_event(ctx->event);
388                 } else {
389                         ast_log(LOG_ERROR, "Event data ended in XML, but event object does not exist!\n");
390                         return 1;
391                 }
392         } else if (!strcmp(name, "Envelope")) {
393                 /* Events end */
394                 ast_debug(3, "EWS: XML: All events has been parsed, merging…\n");
395                 ast_calendar_merge_events(ctx->pvt->owner, ctx->pvt->events);
396         }
397
398         return 0;
399 }
400
401 static const char *mstime(time_t t, char *buf, size_t buflen)
402 {
403         struct timeval tv = {
404                 .tv_sec = t,
405         };
406         struct ast_tm tm;
407
408         ast_localtime(&tv, &tm, "utc");
409         ast_strftime(buf, buflen, "%FT%TZ", &tm);
410
411         return S_OR(buf, "");
412 }
413
414 static const char *msstatus(enum ast_calendar_busy_state state)
415 {
416         switch (state) {
417         case AST_CALENDAR_BS_BUSY_TENTATIVE:
418                 return "Tentative";
419         case AST_CALENDAR_BS_BUSY:
420                 return "Busy";
421         case AST_CALENDAR_BS_FREE:
422                 return "Free";
423         default:
424                 return "";
425         }
426 }
427
428 static const char *get_soap_action(enum xml_op op)
429 {
430         switch (op) {
431         case XML_OP_FIND:
432                 return "\"http://schemas.microsoft.com/exchange/services/2006/messages/FindItem\"";
433         case XML_OP_GET:
434                 return "\"http://schemas.microsoft.com/exchange/services/2006/messages/GetItem\"";
435         case XML_OP_CREATE:
436                 return "\"http://schemas.microsoft.com/exchange/services/2006/messages/CreateItem\"";
437         }
438
439         return "";
440 }
441
442 static int send_ews_request_and_parse(struct ast_str *request, struct xml_context *ctx)
443 {
444         int ret;
445         ne_request *req;
446         ne_xml_parser *parser;
447
448         ast_debug(3, "EWS: HTTP request...\n");
449         if (!(ctx && ctx->pvt)) {
450                 ast_log(LOG_ERROR, "There is no private!\n");
451                 return -1;
452         }
453
454         if (!ast_str_strlen(request)) {
455                 ast_log(LOG_ERROR, "No request to send!\n");
456                 return -1;
457         }
458
459         ast_debug(3, "%s\n", ast_str_buffer(request));
460
461         /* Prepare HTTP POST request */
462         req = ne_request_create(ctx->pvt->session, "POST", ctx->pvt->uri.path);
463         ne_set_request_flag(req, NE_REQFLAG_IDEMPOTENT, 0);
464
465         /* Set headers--should be application/soap+xml, but MS… :/ */
466         ne_add_request_header(req, "Content-Type", "text/xml; charset=utf-8");
467         ne_add_request_header(req, "SOAPAction", get_soap_action(ctx->op));
468
469         /* Set body to SOAP request */
470         ne_set_request_body_buffer(req, ast_str_buffer(request), ast_str_strlen(request));
471
472         /* Prepare XML parser */
473         parser = ne_xml_create();
474         ctx->parser = parser;
475         ne_xml_push_handler(parser, startelm, cdata, endelm, ctx);      /* Callbacks */
476
477         /* Dispatch request and parse response as XML */
478         ret = ne_xml_dispatch_request(req, parser);
479         if (ret != NE_OK) { /* Error handling */
480                 ast_log(LOG_WARNING, "Unable to communicate with Exchange Web Service at '%s': %s\n", ctx->pvt->url, ne_get_error(ctx->pvt->session));
481                 ne_request_destroy(req);
482                 ast_free(request);
483                 ne_xml_destroy(parser);
484                 return -1;
485         }
486
487         /* Cleanup */
488         ne_request_destroy(req);
489         ne_xml_destroy(parser);
490
491         return 0;
492 }
493
494 static int ewscal_write_event(struct ast_calendar_event *event)
495 {
496         struct ast_str *request;
497         struct ewscal_pvt *pvt = event->owner->tech_pvt;
498         char start[21], end[21];
499         struct xml_context ctx = {
500                 .op = XML_OP_CREATE,
501                 .pvt = pvt,
502         };
503         int ret;
504
505         if (!pvt) {
506                 return -1;
507         }
508
509         if (!(request = ast_str_create(1024))) {
510                 return -1;
511         }
512
513         ast_str_set(&request, 0,
514                 "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
515                         "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
516                         "xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
517                         "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
518                         "<soap:Body>"
519                         "<CreateItem xmlns=\"http://schemas.microsoft.com/exchange/services/2006/messages\" "
520                                 "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" "
521                                 "SendMeetingInvitations=\"SendToNone\" >"
522                                 "<SavedItemFolderId>"
523                                         "<t:DistinguishedFolderId Id=\"calendar\"/>"
524                                 "</SavedItemFolderId>"
525                                 "<Items>"
526                                         "<t:CalendarItem xmlns=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
527                                                 "<Subject>%s</Subject>"
528                                                 "<Body BodyType=\"Text\">%s</Body>"
529                                                 "<ReminderIsSet>false</ReminderIsSet>"
530                                                 "<Start>%s</Start>"
531                                                 "<End>%s</End>"
532                                                 "<IsAllDayEvent>false</IsAllDayEvent>"
533                                                 "<LegacyFreeBusyStatus>%s</LegacyFreeBusyStatus>"
534                                                 "<Location>%s</Location>"
535                                         "</t:CalendarItem>"
536                                 "</Items>"
537                         "</CreateItem>"
538                 "</soap:Body>"
539                 "</soap:Envelope>",
540                 event->summary,
541                 event->description,
542                 mstime(event->start, start, sizeof(start)),
543                 mstime(event->end, end, sizeof(end)),
544                 msstatus(event->busy_state),
545                 event->location
546         );
547
548         ret = send_ews_request_and_parse(request, &ctx);
549
550         ast_free(request);
551
552         return ret;
553 }
554
555 static struct calendar_id *get_ewscal_ids_for(struct ewscal_pvt *pvt)
556 {
557         char start[21], end[21];
558         struct ast_tm tm;
559         struct timeval tv;
560         struct ast_str *request;
561         struct xml_context ctx = {
562                 .op = XML_OP_FIND,
563                 .pvt = pvt,
564         };
565
566         ast_debug(5, "EWS: get_ewscal_ids_for()\n");
567
568         if (!pvt) {
569                 ast_log(LOG_ERROR, "There is no private!\n");
570                 return NULL;
571         }
572
573         /* Prepare timeframe strings */
574         tv = ast_tvnow();
575         ast_localtime(&tv, &tm, "UTC");
576         ast_strftime(start, sizeof(start), "%FT%TZ", &tm);
577         tv.tv_sec += 60 * pvt->owner->timeframe;
578         ast_localtime(&tv, &tm, "UTC");
579         ast_strftime(end, sizeof(end), "%FT%TZ", &tm);
580
581         /* Prepare SOAP request */
582         if (!(request = ast_str_create(512))) {
583                 return NULL;
584         }
585
586         ast_str_set(&request, 0,
587                 "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" "
588                 "xmlns:ns1=\"http://schemas.microsoft.com/exchange/services/2006/types\" "
589                 "xmlns:ns2=\"http://schemas.microsoft.com/exchange/services/2006/messages\">"
590                         "<SOAP-ENV:Body>"
591                                 "<ns2:FindItem Traversal=\"Shallow\">"
592                                         "<ns2:ItemShape>"
593                                                 "<ns1:BaseShape>IdOnly</ns1:BaseShape>"
594                                         "</ns2:ItemShape>"
595                                         "<ns2:CalendarView StartDate=\"%s\" EndDate=\"%s\"/>"   /* Timeframe */
596                                         "<ns2:ParentFolderIds>"
597                                                 "<ns1:DistinguishedFolderId Id=\"calendar\"/>"
598                                         "</ns2:ParentFolderIds>"
599                                 "</ns2:FindItem>"
600                         "</SOAP-ENV:Body>"
601                 "</SOAP-ENV:Envelope>",
602                 start, end      /* Timeframe */
603         );
604
605         AST_LIST_HEAD_INIT_NOLOCK(&ctx.ids);
606
607         /* Dispatch request and parse response as XML */
608         if (send_ews_request_and_parse(request, &ctx)) {
609                 ast_free(request);
610                 return NULL;
611         }
612
613         /* Cleanup */
614         ast_free(request);
615
616         return AST_LIST_FIRST(&ctx.ids);
617 }
618
619 static int parse_ewscal_id(struct ewscal_pvt *pvt, const char *id) {
620         struct ast_str *request;
621         struct xml_context ctx = {
622                 .pvt = pvt,
623                 .op = XML_OP_GET,
624         };
625
626         if (!(request = ast_str_create(512))) {
627                 return -1;
628         }
629
630         ast_str_set(&request, 0,
631                 "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
632                 "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" "
633                 "xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">"
634                 "<soap:Body>"
635                         "<GetItem xmlns=\"http://schemas.microsoft.com/exchange/services/2006/messages\">"
636                                 "<ItemShape>"
637                                         "<t:BaseShape>AllProperties</t:BaseShape>"
638                                 "</ItemShape>"
639                                 "<ItemIds>"
640                                         "<t:ItemId Id=\"%s\"/>"
641                                 "</ItemIds>"
642                         "</GetItem>"
643                 "</soap:Body>"
644                 "</soap:Envelope>", id
645         );
646
647         if (send_ews_request_and_parse(request, &ctx)) {
648                 ast_free(request);
649                 return -1;
650         }
651
652         ast_free(request);
653
654         return 0;
655 }
656
657 static int update_ewscal(struct ewscal_pvt *pvt)
658 {
659         struct calendar_id *id_head;
660         struct calendar_id *iter;
661
662         if (!(id_head = get_ewscal_ids_for(pvt))) {
663                 return 0;
664         }
665
666         for (iter = id_head; iter; iter = AST_LIST_NEXT(iter, next)) {
667                 parse_ewscal_id(pvt, ast_str_buffer(iter->id));
668                 ast_free(iter->id);
669                 ast_free(iter);
670         }
671
672         return 0;
673 }
674
675 static void *ewscal_load_calendar(void *void_data)
676 {
677         struct ewscal_pvt *pvt;
678         const struct ast_config *cfg;
679         struct ast_variable *v;
680         struct ast_calendar *cal = void_data;
681         ast_mutex_t refreshlock;
682
683         ast_debug(5, "EWS: ewscal_load_calendar()\n");
684
685         if (!(cal && (cfg = ast_calendar_config_acquire()))) {
686                 ast_log(LOG_ERROR, "You must enable calendar support for res_ewscal to load\n");
687                 return NULL;
688         }
689
690         if (ao2_trylock(cal)) {
691                 if (cal->unloading) {
692                         ast_log(LOG_WARNING, "Unloading module, load_calendar cancelled.\n");
693                 } else {
694                         ast_log(LOG_WARNING, "Could not lock calendar, aborting!\n");
695                 }
696                 ast_calendar_config_release();
697                 return NULL;
698         }
699
700         if (!(pvt = ao2_alloc(sizeof(*pvt), ewscal_destructor))) {
701                 ast_log(LOG_ERROR, "Could not allocate ewscal_pvt structure for calendar: %s\n", cal->name);
702                 ast_calendar_config_release();
703                 return NULL;
704         }
705
706         pvt->owner = cal;
707
708         if (!(pvt->events = ast_calendar_event_container_alloc())) {
709                 ast_log(LOG_ERROR, "Could not allocate space for fetching events for calendar: %s\n", cal->name);
710                 pvt = unref_ewscal(pvt);
711                 ao2_unlock(cal);
712                 ast_calendar_config_release();
713                 return NULL;
714         }
715
716         if (ast_string_field_init(pvt, 32)) {
717                 ast_log(LOG_ERROR, "Couldn't allocate string field space for calendar: %s\n", cal->name);
718                 pvt = unref_ewscal(pvt);
719                 ao2_unlock(cal);
720                 ast_calendar_config_release();
721                 return NULL;
722         }
723
724         for (v = ast_variable_browse(cfg, cal->name); v; v = v->next) {
725                 if (!strcasecmp(v->name, "url")) {
726                         ast_string_field_set(pvt, url, v->value);
727                 } else if (!strcasecmp(v->name, "user")) {
728                         ast_string_field_set(pvt, user, v->value);
729                 } else if (!strcasecmp(v->name, "secret")) {
730                         ast_string_field_set(pvt, secret, v->value);
731                 }
732         }
733
734         ast_calendar_config_release();
735
736         if (ast_strlen_zero(pvt->url)) {
737                 ast_log(LOG_WARNING, "No URL was specified for Exchange Web Service calendar '%s' - skipping.\n", cal->name);
738                 pvt = unref_ewscal(pvt);
739                 ao2_unlock(cal);
740                 return NULL;
741         }
742
743         if (ne_uri_parse(pvt->url, &pvt->uri) || pvt->uri.host == NULL || pvt->uri.path == NULL) {
744                 ast_log(LOG_WARNING, "Could not parse url '%s' for Exchange Web Service calendar '%s' - skipping.\n", pvt->url, cal->name);
745                 pvt = unref_ewscal(pvt);
746                 ao2_unlock(cal);
747                 return NULL;
748         }
749
750         if (pvt->uri.scheme == NULL) {
751                 pvt->uri.scheme = "http";
752         }
753
754         if (pvt->uri.port == 0) {
755                 pvt->uri.port = ne_uri_defaultport(pvt->uri.scheme);
756         }
757
758         ast_debug(3, "ne_uri.scheme     = %s\n", pvt->uri.scheme);
759         ast_debug(3, "ne_uri.host       = %s\n", pvt->uri.host);
760         ast_debug(3, "ne_uri.port       = %u\n", pvt->uri.port);
761         ast_debug(3, "ne_uri.path       = %s\n", pvt->uri.path);
762         ast_debug(3, "user              = %s\n", pvt->user);
763         ast_debug(3, "secret            = %s\n", pvt->secret);
764
765         pvt->session = ne_session_create(pvt->uri.scheme, pvt->uri.host, pvt->uri.port);
766         ne_set_server_auth(pvt->session, auth_credentials, pvt);
767         ne_set_useragent(pvt->session, "Asterisk");
768
769         if (!strcasecmp(pvt->uri.scheme, "https")) {
770                 ne_ssl_trust_default_ca(pvt->session);
771                 ne_ssl_set_verify(pvt->session, ssl_verify, pvt);
772         }
773
774         cal->tech_pvt = pvt;
775
776         ast_mutex_init(&refreshlock);
777
778         /* Load it the first time */
779         update_ewscal(pvt);
780
781         ao2_unlock(cal);
782
783         /* The only writing from another thread will be if unload is true */
784         for (;;) {
785                 struct timeval tv = ast_tvnow();
786                 struct timespec ts = {0,};
787
788                 ts.tv_sec = tv.tv_sec + (60 * pvt->owner->refresh);
789
790                 ast_mutex_lock(&refreshlock);
791                 while (!pvt->owner->unloading) {
792                         if (ast_cond_timedwait(&pvt->owner->unload, &refreshlock, &ts) == ETIMEDOUT) {
793                                 break;
794                         }
795                 }
796                 ast_mutex_unlock(&refreshlock);
797
798                 if (pvt->owner->unloading) {
799                         ast_debug(10, "Skipping refresh since we got a shutdown signal\n");
800                         return NULL;
801                 }
802
803                 ast_debug(10, "Refreshing after %d minute timeout\n", pvt->owner->refresh);
804
805                 update_ewscal(pvt);
806         }
807
808         return NULL;
809 }
810
811 static int load_module(void)
812 {
813         /* Actualy, 0.29.1 is required (because of NTLM authentication), but this
814          * function does not support matching patch version. */
815         if (ne_version_match(0, 29)) {
816                 ast_log(LOG_ERROR, "Exchange Web Service calendar module require neon >= 0.29.1, but %s is installed.\n", ne_version_string());
817                 return AST_MODULE_LOAD_DECLINE;
818         }
819
820         if (ast_calendar_register(&ewscal_tech) && (ne_sock_init() == 0)) {
821                 return AST_MODULE_LOAD_DECLINE;
822         }
823
824         return AST_MODULE_LOAD_SUCCESS;
825 }
826
827 static int unload_module(void)
828 {
829         ne_sock_exit();
830         ast_calendar_unregister(&ewscal_tech);
831
832         return 0;
833 }
834
835 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Asterisk MS Exchange Web Service Calendar Integration",
836         .load = load_module,
837         .unload = unload_module,
838 );