diary

Text-based journaling program
git clone https://git.in0rdr.ch/diary.git
Log | Files | Refs | Pull requests |Archive | README | LICENSE

caldav.c (21545B)


      1 #include "caldav.h"
      2 
      3 CURL* curl;
      4 char access_token[sizeof(char)*2048] = "";
      5 
      6 // Local bind address for receiving OAuth callbacks.
      7 // Reserve 2 chars for the ipv6 square brackets.
      8 char ip[INET6_ADDRSTRLEN], ipstr[INET6_ADDRSTRLEN + 2];
      9 
     10 // Static XML data for CalDav post requests:
     11 // https://write.in0rdr.ch/caldav-calendar-discovery
     12 const char* principal_request = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/'>"
     13                                 "<d:prop><d:current-user-principal/></d:prop>"
     14                                 "</d:propfind>";
     15 const char* homeset_request = "<d:propfind xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>"
     16                               " <d:prop>"
     17                               "  <c:calendar-home-set />"
     18                               " </d:prop>"
     19                               "</d:propfind>";
     20 const char* calendar_request = "<d:propfind xmlns:d='DAV:' xmlns:cs='http://calendarserver.org/ns/' xmlns:c='urn:ietf:params:xml:ns:caldav'>"
     21                                " <d:prop>"
     22                                "  <d:resourcetype />"
     23                                "  <d:displayname />"
     24                                "  <cs:getctag />"
     25                                "  <c:supported-calendar-component-set />"
     26                                " </d:prop>"
     27                                "</d:propfind>";
     28 char* events_request = "<c:calendar-query xmlns:d='DAV:' xmlns:c='urn:ietf:params:xml:ns:caldav'>"
     29                        "<d:prop><d:getetag/><c:calendar-data/></d:prop>"
     30                        "<c:filter><c:comp-filter name='VCALENDAR'>"
     31                        "<c:comp-filter name='VEVENT'>"
     32                        "<c:time-range start='%s' end='%s'/></c:comp-filter>"
     33                        "</c:comp-filter></c:filter></c:calendar-query>";
     34 
     35 static size_t curl_write_mem_callback(void* contents, size_t size, size_t nmemb, void* userp) {
     36     size_t realsize = size * nmemb;
     37     struct curl_mem_chunk* mem = (struct curl_mem_chunk*)userp;
     38 
     39     char* ptr = realloc(mem->memory, mem->size + realsize + 1);
     40     if (!ptr) {
     41         debug("%s", "Not enough memory (realloc in CURLOPT_WRITEFUNCTION returned NULL)");
     42         return 0;
     43     }
     44 
     45     mem->memory = ptr;
     46     memcpy(&(mem->memory[mem->size]), contents, realsize);
     47     mem->size += realsize;
     48     mem->memory[mem->size] = 0;
     49 
     50     return realsize;
     51 }
     52 
     53 // todo
     54 // https://beej.us/guide/bgnet/html
     55 void* get_in_addr(struct sockaddr* sa) {
     56     if (sa->sa_family == AF_INET) {
     57         return &(((struct sockaddr_in*)sa)->sin_addr);
     58     }
     59 
     60     return &(((struct sockaddr_in6*)sa)->sin6_addr);
     61 }
     62 
     63 /* Get access token from "oauth_eval_cmd" and refresh global var */
     64 void get_access_token() {
     65     FILE *fp;
     66     int status;
     67 
     68     fp = popen(CONFIG.oauth_eval_cmd, "r");
     69     if (fp == NULL)
     70         perror("Failure running oauth_eval_cmd in get_access_token()");
     71 
     72     while (fgets(access_token, sizeof(char)*2048, fp) != NULL)
     73         debug("New access token in get_access_token(): %s", access_token);
     74 
     75     // strip any newline characters
     76     access_token[strcspn(access_token, "\n")] = '\0';
     77 
     78     status = pclose(fp);
     79     if (status == -1) {
     80         perror("Failure during pclose in get_access_token()");
     81     }
     82 }
     83 
     84 /* Make a CalDAV request */
     85 char* caldav_req(struct tm* date, char* url, char* http_method, const char* postfields, int depth, bool basicauth, char* content_type, long num_redirects) {
     86     // only support depths 0 or 1
     87     if (depth < 0 || depth > 1) {
     88         return NULL;
     89     }
     90 
     91     CURLcode res;
     92 
     93     curl = curl_easy_init();
     94 
     95     // https://curl.se/libcurl/c/getinmemory.html
     96     struct curl_mem_chunk caldav_resp;
     97     caldav_resp.memory = malloc(1);
     98     if (caldav_resp.memory == NULL) {
     99         perror("malloc failed");
    100         return NULL;
    101     }
    102     caldav_resp.size = 0;
    103 
    104     if (curl) {
    105         // fail if not authenticated, !CURLE_OK
    106         curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
    107         //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
    108         curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method);
    109         debug("curl_easy_perform() request url: %s", url);
    110         curl_easy_setopt(curl, CURLOPT_URL, url);
    111         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_mem_callback);
    112         curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&caldav_resp);
    113 
    114         // construct header fields
    115         struct curl_slist *header = NULL;
    116 
    117         // prefer basicauth credentials when set
    118         if (basicauth) {
    119                 char basicauth [strlen(CONFIG.caldav_username) + strlen(CONFIG.caldav_password) + 2];
    120                 sprintf(basicauth, "%s:%s", CONFIG.caldav_username, CONFIG.caldav_password);
    121                 curl_easy_setopt(curl, CURLOPT_USERPWD, basicauth);
    122         } else {
    123                 // use OAuth credentials
    124                 char bearer_token[strlen("Authorization: Bearer ") + strlen(access_token) + 1];
    125                 sprintf(bearer_token, "Authorization: Bearer %s", access_token);
    126                 header = curl_slist_append(header, bearer_token);
    127         }
    128 
    129         char depth_header[strlen("Depth: 0") + 1];
    130         sprintf(depth_header, "Depth: %i", depth);
    131         header = curl_slist_append(header, depth_header);
    132         curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header);
    133 
    134         if (strcmp(content_type, "") != 0 && content_type != NULL) {
    135                 char contentType[strlen("Content-Type: ") + strlen(content_type) + 1];
    136                 sprintf(contentType, "Content-Type: %s", content_type);
    137                 header = curl_slist_append(header, contentType);
    138         }
    139 
    140         // set postfields, if any
    141         if (postfields != NULL) {
    142             curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields);
    143         }
    144 
    145         // enable redirect following, some providers have certain caldav
    146         // endpoinds on sub-paths
    147         curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    148         curl_easy_setopt(curl, CURLOPT_MAXREDIRS, num_redirects);
    149 
    150         res = curl_easy_perform(curl);
    151 
    152         long response_code = 0;
    153         curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
    154 
    155         // free the memory used for the curl slist
    156         curl_slist_free_all(header);
    157 
    158         curl_easy_cleanup(curl);
    159 
    160         if (res != CURLE_OK) {
    161             debug("curl_easy_perform() failed in caldav_req(): %s", curl_easy_strerror(res));
    162             debug("curl_easy_perform() response code: %li", response_code);
    163             free(caldav_resp.memory);
    164             return NULL;
    165         }
    166     }
    167 
    168     return caldav_resp.memory;
    169 }
    170 
    171 /* Upload event to CalDAV server */
    172 void put_event(struct tm* date, const char* dir, size_t dir_size, char* calendar_uri, bool basicauth) {
    173     // get entry path
    174     char path[100];
    175     char* ppath = path;
    176     char* descr;
    177     long descr_bytes;
    178 
    179     fpath(dir, dir_size, date, &ppath, sizeof path);
    180     if (ppath == NULL) {
    181         debug("Cannot get file path for entry %s", asctime(date));
    182         return;
    183     }
    184 
    185     FILE* fp = fopen(path, "r");
    186     if (fp == NULL) perror("Error opening file");
    187 
    188     fseek(fp, 0, SEEK_END);
    189     descr_bytes = ftell(fp);
    190     rewind(fp);
    191 
    192     size_t descr_label_size = strlen("DESCRIPTION:");
    193     size_t descr_size = descr_bytes + descr_label_size;
    194     descr = calloc(descr_size + 1, sizeof(char));
    195     if (descr == NULL) {
    196         perror("malloc failed");
    197         fclose(fp);
    198         return;
    199     }
    200 
    201     strcat(descr, "DESCRIPTION:");
    202 
    203     size_t items_read = fread(descr + descr_label_size, sizeof(char), descr_bytes, fp);
    204     if (items_read != descr_bytes) {
    205         perror("Error while reading in put_event()");
    206         fclose(fp);
    207         return;
    208     }
    209 
    210     descr[descr_size] = '\0';
    211     char* folded_descr = fold(descr);
    212 
    213     char uid[9];
    214     strftime(uid, sizeof uid, "%Y%m%d", date);
    215 
    216     char* ics = "BEGIN:VCALENDAR\n"
    217                 "PRODID:-//diary\n" // todo: add version
    218                 "VERSION:2.0\n"
    219                 "CALSCALE:GREGORIAN\n"
    220                 "BEGIN:VEVENT\n"
    221                 "UID:%s\n"
    222                 "DTSTART;VALUE=DATE:%s\n"
    223                 "SUMMARY:%s\n"
    224                 "%s\n"
    225                 "END:VEVENT\n"
    226                 "END:VCALENDAR";
    227     char postfields[strlen(ics) + strlen(folded_descr) + 100];
    228     sprintf(postfields, ics,
    229             uid,
    230             uid,
    231             uid, // todo: display first few chars of DESCRIPTION as SUMMARY
    232             folded_descr);
    233 
    234     debug("PUT text/calendar %s", postfields);
    235 
    236     strcat(calendar_uri, uid);
    237     strcat(calendar_uri, ".ics");
    238     char* response = caldav_req(date, calendar_uri, "PUT", postfields, 0, basicauth, "text/calendar", 0);
    239     fclose(fp);
    240     free(folded_descr);
    241     free(descr);
    242 
    243     if (response == NULL) {
    244         debug("%s", "PUT request for event failed in put_event()");
    245     }
    246 
    247     // free memory allocated to store curl response
    248     free(response);
    249 }
    250 
    251 /*
    252 * Sync with CalDAV server.
    253 * Returns the answer char of the confirmation dialogue
    254 * Returns 0 if neither local nor remote file exists.
    255 * Otherwise, returns -1 on error.
    256 */
    257 int caldav_sync(struct tm* date,
    258                  WINDOW* header,
    259                  WINDOW* cal,
    260                  int pad_pos,
    261                  const char* dir,
    262                  size_t dir_size,
    263                  bool confirm) {
    264     pthread_t progress_tid;
    265 
    266     bool oauth_enabled = !(strcmp(CONFIG.oauth_eval_cmd, "") == 0);
    267     bool basicauth_enabled = !(strcmp(CONFIG.caldav_username, "") == 0 || strcmp(CONFIG.caldav_password, "") == 0);
    268     debug("oauth_enabled: %i", oauth_enabled);
    269     debug("basicauth_enabled: %i", basicauth_enabled);
    270 
    271     // check remote server and calendar are defined
    272     if (strcmp(CONFIG.caldav_server, "") == 0 || strcmp(CONFIG.caldav_calendar, "") == 0) {
    273         debug("CONFIG.caldav_server: %s", CONFIG.caldav_server);
    274         debug("CONFIG.caldav_calendar: %s", CONFIG.caldav_calendar);
    275         debug("CONFIG.caldav_username: %s", CONFIG.caldav_username);
    276         debug("CONFIG.caldav_password: %s", CONFIG.caldav_password);
    277         show_info(header, "CalDAV config incomplete, press any key to continue.", NULL);
    278         return -1;
    279     }
    280 
    281     if (oauth_enabled) {
    282         if (strcmp(access_token, "") == 0) {
    283             // get acess token with oauth_eval_cmd
    284             debug("Fetching access token using oauth_eval_cmd %s", CONFIG.oauth_eval_cmd);
    285             get_access_token();
    286             debug("OAuth access token: %s", access_token);
    287         }
    288     }
    289 
    290     pthread_create(&progress_tid, NULL, show_progress, (void*)header);
    291     pthread_detach(progress_tid);
    292 
    293     char* user_principal = caldav_req(date, CONFIG.caldav_server, "PROPFIND", principal_request, 0, basicauth_enabled, "application/xml", 1);
    294     debug("User principal return XML %s", user_principal);
    295 
    296     if (user_principal == NULL) {
    297         // The principal could not be fetched
    298         debug("%s", "Unable to fetch user principal");
    299         show_info(header, "Unable to fetch user principal, press any key to continue.", &progress_tid);
    300         return -1;
    301     }
    302 
    303     char* current_user_principal = extract_xml_content(
    304             user_principal,
    305             "//*[local-name()='current-user-principal']/*[local-name()='href']",
    306             header,
    307             &progress_tid);
    308 
    309     // free memory allocated by curl request
    310     free(user_principal);
    311 
    312     if (current_user_principal == NULL) {
    313         debug("%s", "Unable to parse current user principal");
    314         show_info(header, "Unable to parse current user principal, press any key to continue.", &progress_tid);
    315         return -1;
    316     }
    317 
    318     // extract host without path as basis for further api paths
    319     char *caldav_host; char *caldav_port; char* caldav_host_scheme;
    320     CURLU *h = curl_url(); CURLUcode uc = curl_url_set(h, CURLUPART_URL, CONFIG.caldav_server, 0); uc = curl_url_get(h, CURLUPART_HOST, &caldav_host, 0);
    321     uc = curl_url_get(h, CURLUPART_SCHEME, &caldav_host_scheme, 0);
    322     uc = curl_url_get(h, CURLUPART_PORT, &caldav_port, 0);
    323     debug("Caldav server host name: %s", caldav_host);
    324     debug("Caldav server port: %s", caldav_port);
    325     debug("Caldav server scheme/protocol: %s", caldav_host_scheme);
    326     if (!uc) {
    327         debug("%s", "cur_url_get() failed in caldav_sync()");
    328     }
    329     curl_url_cleanup(h);
    330 
    331     // get the home-set of the user
    332     char uri[300];
    333     if (caldav_port == NULL) {
    334         sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, current_user_principal);
    335     } else {
    336         sprintf(uri, "%s://%s:%s%s", caldav_host_scheme, caldav_host, caldav_port, current_user_principal);
    337     }
    338 
    339     char* home_set = caldav_req(date, uri, "PROPFIND", homeset_request, 0, basicauth_enabled, "application/xml", 0);
    340     debug("Home set xml %s", home_set);
    341 
    342     // parse home set from xml response
    343     char* home_set_parsed = extract_xml_content(
    344             home_set,
    345             "//*[local-name()='calendar-home-set']/*[local-name()='href']",
    346             header,
    347             &progress_tid);
    348 
    349     if (home_set_parsed == NULL) {
    350         debug("%s", "Unable to parse home set");
    351         show_info(header, "Unable to parse home set, press any key to continue.", &progress_tid);
    352         return -1;
    353     }
    354 
    355     if (caldav_port == NULL) {
    356         sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, home_set_parsed);
    357     } else {
    358         sprintf(uri, "%s://%s:%s%s", caldav_host_scheme, caldav_host, caldav_port, home_set_parsed);
    359     }
    360 
    361     free(home_set);
    362     free(home_set_parsed);
    363 
    364     // get calendar from home-set
    365     char* calendar_xml = caldav_req(date, uri, "PROPFIND", calendar_request, 1, basicauth_enabled, "application/xml", 0);
    366     debug("Calendar set xml: %s", calendar_xml);
    367 
    368     // get calendar URI from the home-set
    369     char calendar_xpath_query[300];
    370     sprintf(calendar_xpath_query, "//*[local-name()='displayname'][text()='%s']/../../../*[local-name()='href']", CONFIG.caldav_calendar);
    371     debug("Calendar xpath query: %s", calendar_xpath_query);
    372     char* calendar_href = extract_xml_content(
    373             calendar_xml,
    374             calendar_xpath_query,
    375             header,
    376             &progress_tid);
    377 
    378     if (calendar_href == NULL) {
    379         debug("Could not find CalDAV calendar %s", CONFIG.caldav_calendar);
    380         char* msg_fmtstr = "Could not find CalDAV calendar '%s', press any key to continue.";
    381         char msg[strlen(msg_fmtstr) + strlen(CONFIG.caldav_calendar)];
    382         sprintf(msg, msg_fmtstr, CONFIG.caldav_calendar);
    383         show_info(header, msg, &progress_tid);
    384         return -1;
    385     }
    386 
    387     free(calendar_xml);
    388 
    389     char dstr_start[30];
    390     char dstr_end[30];
    391     char* format_start = "%Y%m%dT000000Z"; // start of day
    392     char* format_end = "%Y%m%dT235900Z"; // end of day
    393     strftime(dstr_start, sizeof dstr_start, format_start, date);
    394     strftime(dstr_end, sizeof dstr_end, format_end, date);
    395 
    396     char caldata_postfields[strlen(events_request) + strlen(dstr_start) + strlen(dstr_end)];
    397     sprintf(caldata_postfields, events_request,
    398             dstr_start,
    399             dstr_end);
    400 
    401     // fetch event for the cursor date
    402     if (caldav_port == NULL) {
    403         sprintf(uri, "%s://%s%s", caldav_host_scheme, caldav_host, calendar_href);
    404     } else {
    405         sprintf(uri, "%s://%s:%s%s", caldav_host_scheme, caldav_host, caldav_port, calendar_href);
    406     }
    407 
    408     char* events_xml = caldav_req(date, uri, "REPORT", caldata_postfields, 1, basicauth_enabled, "application/xml", 0);
    409 
    410     if (events_xml == NULL) {
    411         debug("%s", "Events could not be fetched");
    412         show_info(header, "Events could not be fetched", &progress_tid);
    413         return -1;
    414     }
    415 
    416     // only fetch PRODID diary events
    417     char* event = extract_xml_content(
    418             events_xml,
    419             "//*[local-name()='calendar-data' and contains(text(), 'PRODID:-//diary')]",
    420             header,
    421             &progress_tid);
    422 
    423     free(events_xml);
    424 
    425     // no remote event found
    426     if (event == NULL) {
    427         debug("%s", "No remote events found, continuing.");
    428         event = "";
    429     }
    430 
    431     // get path of entry
    432     char path[100];
    433     char* ppath = path;
    434     fpath(CONFIG.dir, strlen(CONFIG.dir), date, &ppath, sizeof path);
    435 
    436     time_t epoch_zero = 0;
    437 
    438     // assume local file does not exist
    439     struct tm* localfile_time = gmtime(&epoch_zero);
    440 
    441     // check last modification time of local time
    442     struct stat attr;
    443     if (stat(path, &attr) == 0) {
    444         // if local file does exists, remember utc time of file on disk
    445         localfile_time = gmtime(&attr.st_mtime);
    446     }
    447 
    448     time_t localfile_date = mktime(localfile_time);
    449 
    450     struct tm* remote_datetime;
    451     time_t remote_date = 0;
    452     long search_pos = 0;
    453 
    454     // check remote DTSTAMP of remote event
    455     char* remote_dtstamp = extract_ical_field(event, "DTSTAMP", &search_pos, false);
    456     char* remote_uid = extract_ical_field(event, "UID", &search_pos, false);
    457     debug("Event: %s", event);
    458     debug("DTSTAMP field: %s", remote_dtstamp);
    459 
    460     // init remote date to 1970, assume remote file does not exist
    461     remote_datetime = gmtime(&epoch_zero);
    462 
    463     if (remote_dtstamp != NULL) {
    464         // if remote date does exist, set date to the correct timestamp
    465         strptime(remote_dtstamp, "%Y%m%dT%H%M%SZ", remote_datetime);
    466         free(remote_dtstamp);
    467     }
    468 
    469     remote_date = mktime(remote_datetime);
    470 
    471     debug("Remote last modified: %s", ctime(&remote_date));
    472     debug("Local last modified: %s", ctime(&localfile_date));
    473     double timediff = difftime(localfile_date, remote_date);
    474     debug("Time diff between local and remote mod time: %f", timediff);
    475 
    476     char* rmt_desc;
    477     char dstr[16];
    478     int conf_ch = 0;
    479 
    480     if (timediff == 0) {
    481         debug("%s", "Local and remote files have equal timestamp or don't exist, giving up.");
    482         pthread_cancel(progress_tid);
    483         wclear(header);
    484         // free memory allocated to store curl response
    485         curl_free(caldav_host_scheme);
    486         curl_free(caldav_host);
    487         curl_free(caldav_port);
    488         free(remote_uid);
    489         return 0;
    490     } else if (timediff > 0) {
    491         debug("%s", "Local file is newer. Uploading to remote.");
    492         if (remote_uid) {
    493             // purge any existing daily calendar entries on the remote side
    494             char event_uri[300];
    495             if (caldav_port == NULL) {
    496                 sprintf(event_uri, "%s://%s:%s%s.ics", caldav_host_scheme, caldav_host, calendar_href, remote_uid);
    497             } else {
    498                 sprintf(event_uri, "%s://%s:%s%s%s.ics", caldav_host_scheme, caldav_host, caldav_port, calendar_href, remote_uid);
    499             }
    500             //sprintf(event_uri, "%s%s%s.ics", CONFIG.caldav_server, calendar_href, remote_uid);
    501             debug("Event URI for DELETE request: %s", event_uri);
    502             char* response = caldav_req(date, event_uri, "DELETE", NULL, 0, basicauth_enabled, "", 0);
    503             free(response);
    504         }
    505 
    506         put_event(date, dir, dir_size, uri, basicauth_enabled);
    507 
    508         pthread_cancel(progress_tid);
    509         wclear(header);
    510 
    511     } else if (timediff < 0) {
    512         rmt_desc = extract_ical_field(event, "DESCRIPTION", &search_pos, true);
    513 
    514         if (rmt_desc == NULL) {
    515             debug("%s", "Could not fetch description of remote event. Aborting sync.");
    516             pthread_cancel(progress_tid);
    517             wclear(header);
    518             curl_free(caldav_host_scheme);
    519             curl_free(caldav_host);
    520             free(remote_uid);
    521             return -1;
    522         }
    523 
    524         if (confirm) {
    525             // prepare header for confirmation dialogue
    526             curs_set(2);
    527             noecho();
    528             pthread_cancel(progress_tid);
    529             wclear(header);
    530 
    531             // ask for confirmation
    532             strftime(dstr, sizeof dstr, CONFIG.fmt, date);
    533             mvwprintw(header, 0, 0, "Remote event is more recent. Sync entry '%s' and overwrite local file? [(Y)es/(a)ll/(n)o/(c)ancel] ", dstr);
    534             conf_ch = wgetch(header);
    535         }
    536 
    537         if (conf_ch == 'y' || conf_ch == 'Y' || conf_ch == 'a' || conf_ch == '\n' || !confirm) {
    538             debug("%s", "Remote file is newer. Extracting description from remote.");
    539             char* i;
    540 
    541             // persist downloaded buffer to local file
    542             FILE* cursordate_file = fopen(path, "wb");
    543             if (cursordate_file == NULL) {
    544                 perror("Failed to open cursor date file");
    545             } else {
    546                 for (i = rmt_desc; *i != '\0'; i++) {
    547                     if (rmt_desc[i-rmt_desc] == 0x5C) { // backslash
    548                         switch (*(i+1)) {
    549                             case 'n':
    550                                 fputc('\n', cursordate_file);
    551                                 i++;
    552                                 break;
    553                             case 0x5c: // preserve real backslash
    554                                 fputc(0x5c, cursordate_file);
    555                                 i++;
    556                                 break;
    557                         }
    558                     } else {
    559                         fputc(*i, cursordate_file);
    560                     }
    561                 }
    562             }
    563             fclose(cursordate_file);
    564 
    565             // add new entry highlight
    566             chtype atrs = winch(cal) & A_ATTRIBUTES;
    567             wchgat(cal, 2, atrs | A_BOLD, 0, NULL);
    568             prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH);
    569         }
    570 
    571         echo();
    572         curs_set(0);
    573         free(rmt_desc);
    574     }
    575 
    576     // free memory allocated to store curl response
    577     curl_free(caldav_host_scheme);
    578     curl_free(caldav_host);
    579     free(remote_uid);
    580     free(calendar_href);
    581     pthread_join(progress_tid, NULL);
    582     wclear(header);
    583     return conf_ch;
    584 }