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