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