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