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