[balsa/gmime3: 1/49] Move to libical



commit f7ee53d02959aefd818788b1a2eca10cf85a5958
Author: Albrecht Dreß <albrecht dress arcor de>
Date:   Sun Jan 19 14:12:06 2020 -0500

    Move to libical
    
    finally, I have a patch ready which uses libical for displaying
    text/calendar parts in Balsa.  The old code was /very/ limited and had
    almost no capabilities for displaying complex requests.
    
    Although the new implementation eliminates the code for scanning the
    events, I added a few more fields from the events as well as a *lot*
    of code for decoding recurrence rules which may be *very* complex
    (compare RFC 5545, Sect. 3.3.1).  Actually, I implemented most of this
    stuff (which amounts to ~½ of libbalsa/rfc2445.c) as in Thunderbird's
    Lightning extension, so I /hope/ it covers most common cases.  I have a
    bunch of test messages, created in Thunderbird, to demonstrate parsing,
    just let me know if you would like to check them (or try yourself…).
    
    * README: fix package requirements
    * configure.ac, meson.build: require libical >= 3.0.0 (note:
      not tested for Meson builds)
    * libbalsa/rfc2445.[ch]: mostly re-written, using the libical
      parser and data types
    * src/balsa-mime-widget-vcalendar.c: add additional event items;
      use the new rfc2445.h api
    * src/balsa-print-object-text.c: fix two pango test layout memory
      leaks; add additional event items; use the new rfc2445.h api
    * src/print-gtk.c: fix part selection for printing text/calendar

 ChangeLog                         |   13 +
 README                            |    9 +-
 configure.ac                      |    1 +
 libbalsa/rfc2445.c                | 1380 +++++++++++++++++++++----------------
 libbalsa/rfc2445.h                |   63 +-
 meson.build                       |    4 +-
 src/balsa-mime-widget-vcalendar.c |   71 +-
 src/balsa-print-object-text.c     |   63 +-
 src/print-gtk.c                   |   20 +-
 9 files changed, 958 insertions(+), 666 deletions(-)
---
diff --git a/ChangeLog b/ChangeLog
index e69cbaca7..c3500bf8b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,16 @@
+2020-01-19  Albrecht Dreß  <albrecht dress arcor de>
+
+       * README: fix package requirements
+       * configure.ac, meson.build: require libical >= 3.0.0 (note:
+         not tested for Meson builds)
+       * libbalsa/rfc2445.[ch]: mostly re-written, using the libical
+         parser and data types
+       * src/balsa-mime-widget-vcalendar.c: add additional event items;
+         use the new rfc2445.h api
+       * src/balsa-print-object-text.c: fix two pango test layout memory
+         leaks; add additional event items; use the new rfc2445.h api
+       * src/print-gtk.c: fix part selection for printing text/calendar
+
 2020-01-19  Peter Bloomfield  <pbloomfield bellsouth net>
 
        balsa-index: Simplify managing idle handlers
diff --git a/README b/README
index 1556c1313..8e6dd3ba2 100644
--- a/README
+++ b/README
@@ -3,7 +3,7 @@ Balsa E-Mail Client 2.5.x
 
 See ChangeLog for the list of the recent changes and NEWS for highlights.
 
-Copyright (C) 1997-2019 Stuart Parmenter and others
+Copyright (C) 1997-2020 Stuart Parmenter and others
 
 See 'COPYING' for licence information.
 
@@ -39,12 +39,15 @@ should run './configure --help' to get an idea of them. More
 complete descriptions are here.
 
 Basically, Balsa requires
-- glib-2.0 >= 2.40.0
-- gtk+-3.0 >= 3.10.0
+- glib-2.0 >= 2.48.0
+- gtk+-3.0 >= 3.18.0
 - gmime-2.6
 - gio-2.0
 - gthread-2.0
+- gnutls >= 3.0
 - gpgme >= 1.6.0
+- libical >= 3.0.0
+- fribidi
 
 --disable-more-warnings
        Balsa by default is very sensitive to compilation warnings
diff --git a/configure.ac b/configure.ac
index d9452b288..0ab69bb36 100644
--- a/configure.ac
+++ b/configure.ac
@@ -230,6 +230,7 @@ gio-2.0
 gthread-2.0
 gnutls >= 3.0
 fribidi
+libical >= 3.0.0
 ])
 
 PKG_CHECK_MODULES(BALSA_AB, [
diff --git a/libbalsa/rfc2445.c b/libbalsa/rfc2445.c
index 46dee8855..ee358b236 100644
--- a/libbalsa/rfc2445.c
+++ b/libbalsa/rfc2445.c
@@ -1,7 +1,7 @@
 /* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
 /*
- * VCalendar (RFC 2445) stuff
- * Copyright (C) 2009-2019 Albrecht Dreß <albrecht dress arcor de>
+ * VCalendar (RFC 5545 and 5546) stuff
+ * Copyright (C) 2009-2020 Albrecht Dreß <albrecht dress arcor de>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
 # include "config.h"
 #endif                          /* HAVE_CONFIG_H */
-#include "rfc2445.h"
 
 #include <stdlib.h>
 #include <string.h>
@@ -29,24 +28,17 @@
 #include <glib-object.h>
 
 #include "libbalsa.h"
-#include "missing.h"
-
-
-/* participant roles as defined by RFC 2446 */
-typedef enum {
-    VCAL_ROLE_UNKNOWN = 0,
-    VCAL_ROLE_CHAIR,
-    VCAL_ROLE_REQ_PART,
-    VCAL_ROLE_OPT_PART,
-    VCAL_ROLE_NON_PART
-} LibBalsaVCalRole;
+#include "rfc2445.h"
 
 
 struct _LibBalsaVCal {
     GObject parent;
 
     /* method */
-    LibBalsaVCalMethod method;
+    icalproperty_method method;
+
+    /* VCALENDAR object */
+    icalcomponent *vcalendar;
 
     /* linked list of VEVENT entries */
     GList *vevent;
@@ -60,15 +52,19 @@ struct _LibBalsaVEvent {
 
     LibBalsaAddress *organizer;
     GList *attendee;
-    time_t stamp;
-    time_t start;
-    gboolean start_date_only;
-    time_t end;
-    gboolean end_date_only;
+    icaltimetype stamp;
+    icaltimetype start;
+    icaltimetype end;
+    icaltimetype recurrence_id;
+    struct icaldurationtype duration;
+    struct icalrecurrencetype rrule;
+    icalproperty_status status;
     gchar *uid;
+    int sequence;
     gchar *summary;
     gchar *location;
     gchar *description;
+    GList *categories;
 };
 
 G_DEFINE_TYPE(LibBalsaVEvent, libbalsa_vevent, G_TYPE_OBJECT)
@@ -76,46 +72,23 @@ G_DEFINE_TYPE(LibBalsaVEvent, libbalsa_vevent, G_TYPE_OBJECT)
 
 /* LibBalsaAddress extra object data */
 typedef struct {
-    LibBalsaVCalRole role;
-    LibBalsaVCalPartStat part_stat;
-    gboolean rsvp;
+       icalparameter_role role;
+    icalparameter_partstat part_stat;
+    icalparameter_rsvp rsvp;
 } LibBalsaVCalInfo;
 #define RFC2445_INFO            "RFC2445:Info"
 
 
 static void libbalsa_vcal_finalize(GObject *self);
-
 static void libbalsa_vevent_finalize(GObject *self);
 
-static LibBalsaAddress *cal_address_2445_to_lbaddress(const gchar * uri,
-                                                     gchar ** attributes,
-                                                     gboolean
-                                                     is_organizer);
-
 /* conversion helpers */
-static time_t date_time_2445_to_time_t(const gchar *date_time, const gchar *modifier, gboolean *date_only);
-static gchar *time_t_to_date_time_2445(time_t ttime);
-static gchar *text_2445_unescape(const gchar * text);
-static gchar *text_2445_escape(const gchar * text);
-static const gchar *vcal_role_to_str(LibBalsaVCalRole role);
-static LibBalsaVCalMethod vcal_str_to_method(const gchar * method);
-static LibBalsaVCalRole vcal_str_to_role(const gchar * role);
-static LibBalsaVCalPartStat vcal_str_to_part_stat(const gchar * pstat);
-
-
-static struct {
-    gchar *str_2445;
-    gchar *hr_text;
-} pstats[] = {
-    { NULL, N_("unknown") },
-    { "NEEDS-ACTION", N_("needs action") },
-    { "ACCEPTED", N_("accepted") },
-    { "DECLINED", N_("declined") },
-    { "TENTATIVE", N_("tentatively accepted") },
-    { "DELEGATED", N_("delegated") },
-    { "COMPLETED", N_("completed") },
-    { "IN-PROCESS", N_("in process") }
-};
+static LibBalsaVCal *vcalendar_extract(const gchar *vcal_buf);
+static LibBalsaAddress *cal_address_5545_to_lbaddress(icalproperty *prop,
+                                                                                                         
gboolean      is_organizer);
+static const gchar *vcal_role_to_str(icalparameter_role role);
+static gchar *icaltime_str(icaltimetype  ical_time,
+                                                  const gchar  *format_str);
 
 
 /* --- VCal GObject stuff --- */
@@ -132,8 +105,7 @@ libbalsa_vcal_class_init(LibBalsaVCalClass *klass)
 static void
 libbalsa_vcal_init(LibBalsaVCal *self)
 {
-    self->method = ITIP_UNKNOWN;
-    self->vevent = NULL;
+    self->method = ICAL_METHOD_NONE;
 }
 
 
@@ -146,18 +118,14 @@ libbalsa_vcal_finalize(GObject *self)
     if (vcal->vevent != NULL) {
        g_list_free_full(vcal->vevent, g_object_unref);
     }
+    if (vcal->vcalendar != NULL) {
+       icalcomponent_free(vcal->vcalendar);
+    }
 
     (*parent_class->finalize)(self);
 }
 
 
-LibBalsaVCal *
-libbalsa_vcal_new(void)
-{
-    return LIBBALSA_VCAL(g_object_new(LIBBALSA_TYPE_VCAL, NULL));
-}
-
-
 /* --- VEvent GObject stuff --- */
 static void
 libbalsa_vevent_class_init(LibBalsaVEventClass * klass)
@@ -171,7 +139,12 @@ libbalsa_vevent_class_init(LibBalsaVEventClass * klass)
 static void
 libbalsa_vevent_init(LibBalsaVEvent *self)
 {
-    self->start = self->end = self->stamp = (time_t) (-1);
+    self->start = icaltime_null_time();
+    self->end = icaltime_null_time();
+    self->stamp = icaltime_null_time();
+    self->duration = icaldurationtype_null_duration();
+    self->rrule.freq = ICAL_NO_RECURRENCE;
+    self->status = ICAL_STATUS_NONE;
 }
 
 
@@ -191,193 +164,42 @@ libbalsa_vevent_finalize(GObject *self)
        g_free(vevent->summary);
        g_free(vevent->location);
        g_free(vevent->description);
+       g_list_free_full(vevent->categories, g_free);
 
        (*parent_class->finalize)(self);
 }
 
 
-LibBalsaVEvent *
-libbalsa_vevent_new(void)
-{
-    return LIBBALSA_VEVENT(g_object_new(LIBBALSA_TYPE_VEVENT, NULL));
-}
-
-
-#define STR_REPL_2445_TXT(s, v)                 \
-    do {                                        \
-        g_free(s);                              \
-        s = text_2445_unescape(v);              \
-    } while (0)
-
-/*
- * Find the first occurrence of c in s that is not in a double-quoted
- * string.
- *
- * Note that if c is '\0', the return value is NULL, *not* a pointer to
- * the terminating '\0' (c.f. strchr).
- */
-static gchar *
-vcal_strchr(gchar * s, gint c)
-{
-    gboolean is_quoted = FALSE;
-
-    while (*s) {
-        if (*s == '"')
-            is_quoted = !is_quoted;
-        else if (*s == c && !is_quoted)
-            return s;
-        ++s;
-    }
-
-    return NULL;
-}
-
-/*
- * Split the string s at unquoted occurrences of c.
- *
- * Note that c is a single character, not a string, as in g_strsplit,
- * and that there is no limit on the number of splits.
- *
- * Returns a newly-allocated NULL-terminated array of strings. Use
- * g_strfreev() to free it.
- */
-static gchar **
-vcal_strsplit_at_char(gchar * s, gint c)
-{
-    GPtrArray *array = g_ptr_array_new();
-    gchar *p;
-
-    while ((p = vcal_strchr(s, c))) {
-        g_ptr_array_add(array, g_strndup(s, p - s));
-        s = p + 1;
-    }
-    g_ptr_array_add(array, g_strdup(s));
-    g_ptr_array_add(array, NULL);
-
-    return (gchar **) g_ptr_array_free(array, FALSE);
-}
-
 /* parse a text/calendar part and convert it into a LibBalsaVCal object */
 LibBalsaVCal *
 libbalsa_vcal_new_from_body(LibBalsaMessageBody * body)
 {
-    LibBalsaVCal *retval;
-    gchar *charset;
-    gchar *method;
+    LibBalsaVCal *retval = NULL;
     gchar *vcal_buf;
-    gchar *p;
-    gchar **lines;
-    LibBalsaVEvent *event;
-    gboolean in_embedded;
-    int k;
 
     g_return_val_if_fail(body != NULL, NULL);
 
     /* get the body buffer */
-    if (libbalsa_message_body_get_content(body, &vcal_buf, NULL) <= 0)
-       return NULL;
-
-    /* check if the body has a charset (default is utf-8, see '2445,
-     * sect 4.1.4) */
-    charset = libbalsa_message_body_get_parameter(body, "charset");
-    if (charset && g_ascii_strcasecmp(charset, "utf-8")) {
-       gsize written;
-       gchar *conv_buf;
-
-       conv_buf =
-           g_convert(vcal_buf, -1, "utf-8", charset, NULL, &written,
-                     NULL);
-       g_free(vcal_buf);
-       vcal_buf = conv_buf;
-    }
-    g_free(charset);
-    if (!vcal_buf)
-       return NULL;
-
-    /* o.k., create a new object */
-    retval = libbalsa_vcal_new();
-
-    /* replace \r\n by \n */
-    p = strchr(vcal_buf, '\r');
-    while (p) {
-        if (p[1] =='\n')
-           memmove(p, p + 1, strlen(p + 1) + 1);
-       p = strchr(p + 1, '\r');
-    }
+    if (libbalsa_message_body_get_content(body, &vcal_buf, NULL) > 0) {
+       gchar *charset;
 
-    /* unfold the body and split into lines */
-    p = strchr(vcal_buf, '\n');
-    while (p) {
-       if (p[1] == '\t' || p[1] == ' ')
-           memmove(p, p + 2, strlen(p + 2) + 1);
-       p = strchr(p + 1, '\n');
-    }
-    lines = g_strsplit(vcal_buf, "\n", -1);
-    g_free(vcal_buf);
-
-    /* scan lines to extract vevent(s) */
-    event = NULL;
-    method = NULL;
-    in_embedded = FALSE;
-    for (k = 0; lines[k]; k++) {
-       if (!event) {
-            if (!method && !g_ascii_strncasecmp("METHOD:", lines[k], 7))
-                method = g_strdup(lines[k] + 7);
-           if (!g_ascii_strcasecmp("BEGIN:VEVENT", lines[k]))
-               event = libbalsa_vevent_new();
-       } else if (strlen(lines[k])) {
-           gchar *value = vcal_strchr(lines[k], ':');
-           gchar **entry;
-
-           if (value)
-               *value++ = '\0';
-           entry = vcal_strsplit_at_char(lines[k], ';');
-           if (!g_ascii_strcasecmp(entry[0], "END")) {
-                if (value && !g_ascii_strcasecmp(value, "VEVENT")) {
-                    retval->vevent = g_list_append(retval->vevent, event);
-                    event = NULL;
-                } else {
-                    in_embedded = FALSE;
-                }
-            } else if (!g_ascii_strcasecmp(entry[0], "BEGIN"))
-                in_embedded = TRUE;
-            else if (!in_embedded) {
-                if (!g_ascii_strcasecmp(entry[0], "DTSTART"))
-                    event->start = date_time_2445_to_time_t(value, entry[1], &event->start_date_only);
-                else if (!g_ascii_strcasecmp(entry[0], "DTEND"))
-                    event->end = date_time_2445_to_time_t(value, entry[1], &event->end_date_only);
-                else if (!g_ascii_strcasecmp(entry[0], "DTSTAMP"))
-                    event->stamp = date_time_2445_to_time_t(value, entry[1], NULL);
-                else if (!g_ascii_strcasecmp(entry[0], "UID"))
-                    STR_REPL_2445_TXT(event->uid, value);
-                else if (!g_ascii_strcasecmp(entry[0], "SUMMARY"))
-                    STR_REPL_2445_TXT(event->summary, value);
-                else if (!g_ascii_strcasecmp(entry[0], "LOCATION"))
-                    STR_REPL_2445_TXT(event->location, value);
-                else if (!g_ascii_strcasecmp(entry[0], "DESCRIPTION"))
-                    STR_REPL_2445_TXT(event->description, value);
-                else if (!g_ascii_strcasecmp(entry[0], "ORGANIZER")) {
-                    if (event->organizer)
-                        g_object_unref(event->organizer);
-                    event->organizer =
-                        cal_address_2445_to_lbaddress(value, entry + 1, TRUE);
-                } else if (!g_ascii_strcasecmp(entry[0], "ATTENDEE"))
-                    event->attendee =
-                        g_list_prepend(event->attendee,
-                                       cal_address_2445_to_lbaddress(value,
-                                                                     entry + 1,
-                                                                     FALSE));
-            }
-           g_strfreev(entry);
-       }
-    }
-    g_strfreev(lines);
-    if (event)
-       g_object_unref(event);
+       /* check if the body has a charset (default is utf-8, see '2445, sect 4.1.4) */
+       charset = libbalsa_message_body_get_parameter(body, "charset");
+       if ((charset != NULL) && (g_ascii_strcasecmp(charset, "utf-8") != 0)) {
+               gchar *conv_buf;
+
+               conv_buf = g_convert(vcal_buf, -1, "utf-8", charset, NULL, NULL, NULL);
+               g_free(vcal_buf);
+               vcal_buf = conv_buf;
+       }
+       g_free(charset);
 
-    /* set the method */
-    retval->method = vcal_str_to_method(method);
-    g_free(method);
+       if (vcal_buf != NULL) {
+               /* o.k., create a new object */
+               retval = vcalendar_extract(vcal_buf);
+               g_free(vcal_buf);
+       }
+    }
 
     return retval;
 }
@@ -386,7 +208,7 @@ libbalsa_vcal_new_from_body(LibBalsaMessageBody * body)
 /* return a rfc 2445 attendee (i.e. a LibBalsaAddress w/ extra information)
  * as a human-readable string */
 gchar *
-libbalsa_vcal_attendee_to_str(LibBalsaAddress * person)
+libbalsa_vcal_attendee_to_str(LibBalsaAddress *person)
 {
     GString *retval;
     gchar *str;
@@ -397,16 +219,17 @@ libbalsa_vcal_attendee_to_str(LibBalsaAddress * person)
     retval = g_string_new("");
 
     info = g_object_get_data(G_OBJECT(person), RFC2445_INFO);
-    if (info->role != VCAL_ROLE_UNKNOWN)
-       g_string_printf(retval, "%s ", vcal_role_to_str(info->role));
+    if (info->role != ICAL_ROLE_NONE) {
+       g_string_printf(retval, "%s ", vcal_role_to_str(info->role));
+    }
 
     str = libbalsa_address_to_gchar(person, -1);
     retval = g_string_append(retval, str);
     g_free(str);
 
-    if (info->part_stat != VCAL_PSTAT_UNKNOWN)
-       g_string_append_printf(retval, " (%s)",
-                              libbalsa_vcal_part_stat_to_str(info->part_stat));
+    if (info->part_stat != ICAL_PARTSTAT_NONE) {
+       g_string_append_printf(retval, " (%s)", libbalsa_vcal_part_stat_to_str(info->part_stat));
+    }
 
     return g_string_free(retval, FALSE);
 }
@@ -420,10 +243,8 @@ libbalsa_vcal_attendee_rsvp(LibBalsaAddress * person)
     LibBalsaVCalInfo *info;
 
     g_return_val_if_fail(LIBBALSA_IS_ADDRESS(person), FALSE);
-
     info = g_object_get_data(G_OBJECT(person), RFC2445_INFO);
-
-    return info->rsvp;
+    return info->rsvp == ICAL_RSVP_TRUE;
 }
 
 
@@ -459,50 +280,27 @@ libbalsa_vevent_description(LibBalsaVEvent *event)
 }
 
 
-time_t
-libbalsa_vevent_timestamp(LibBalsaVEvent *event, LibBalsaVEventTimestamp which)
+gchar *
+libbalsa_vevent_time_str(LibBalsaVEvent *event, LibBalsaVEventTimestamp which, const gchar *format_str)
 {
-       time_t result;
-
-       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event) &&
-               (which >= VEVENT_DATETIME_STAMP) && (VEVENT_DATETIME_END), (time_t) -1);
-       switch (which) {
-       case VEVENT_DATETIME_STAMP:
-               result = event->stamp;
-               break;
-       case VEVENT_DATETIME_START:
-               result = event->start;
-               break;
-       case VEVENT_DATETIME_END:
-               result = event->end;
-               break;
-       default:
-               g_assert_not_reached();
-       }
-       return result;
-}
+       gchar *result = NULL;
 
+       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event), NULL);
 
-gboolean
-libbalsa_vevent_timestamp_date_only(LibBalsaVEvent *event, LibBalsaVEventTimestamp which)
-{
-       gboolean result;
-
-       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event) &&
-               (which >= VEVENT_DATETIME_STAMP) && (VEVENT_DATETIME_END), FALSE);
        switch (which) {
        case VEVENT_DATETIME_STAMP:
-               result = FALSE;
+               result = icaltime_str(event->stamp, format_str);
                break;
        case VEVENT_DATETIME_START:
-               result = event->start_date_only;
+               result = icaltime_str(event->start, format_str);
                break;
        case VEVENT_DATETIME_END:
-               result = event->end_date_only;
+               result = icaltime_str(event->end, format_str);
                break;
        default:
                g_assert_not_reached();
        }
+
        return result;
 }
 
@@ -523,381 +321,807 @@ libbalsa_vevent_attendee(LibBalsaVEvent *event, guint nth_attendee)
 }
 
 
-/* return a new buffer containing a proper reply to an event for a new
- * participant status */
+guint
+libbalsa_vevent_category_count(LibBalsaVEvent *event)
+{
+       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event), 0U);
+       return g_list_length(event->categories);
+}
+
+
 gchar *
-libbalsa_vevent_reply(const LibBalsaVEvent * event, const gchar * sender,
-                     LibBalsaVCalPartStat new_stat)
+libbalsa_vevent_category_str(LibBalsaVEvent *event)
 {
-    GString *retval;
-    gchar *buf;
-    gssize p;
-    gchar *eol;
+       gchar *res = NULL;
 
-    /* check for allowed new state and sender */
-    g_return_val_if_fail((new_stat == VCAL_PSTAT_ACCEPTED ||
-                         new_stat == VCAL_PSTAT_DECLINED ||
-                         new_stat == VCAL_PSTAT_TENTATIVE) &&
-                        sender != NULL, NULL);
+       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event), NULL);
+       if (event->categories != NULL) {
+               GString *buffer;
+               GList *p;
+
+               buffer = g_string_new((const gchar *) event->categories->data);
+               for (p = event->categories->next; p != NULL; p = p->next) {
+                       g_string_append_c(buffer, '\n');
+                       g_string_append(buffer, (const gchar *) p->data);
+               }
+               res = g_string_free(buffer, FALSE);
+       }
+       return res;
+}
 
-    /* build the part */
-    retval = g_string_new("BEGIN:VCALENDAR\nVERSION:2.0\n"
-                         "PRODID:-//GNOME//Balsa " BALSA_VERSION "//EN\n"
-                         "METHOD:REPLY\nBEGIN:VEVENT\n");
-    buf = time_t_to_date_time_2445(time(NULL));
-    g_string_append_printf(retval, "DTSTAMP:%s\n", buf);
-    g_free(buf);
-    g_string_append_printf(retval, "ATTENDEE;PARTSTAT=%s:mailto:%s\n";,
-                          pstats[(int) new_stat].str_2445, sender);
-    if (event->uid)
-       g_string_append_printf(retval, "UID:%s\n", event->uid);
-    if (event->organizer != NULL) {
-        const gchar *addr = libbalsa_address_get_addr(event->organizer);
 
-        if (addr != NULL)
-            g_string_append_printf(retval, "ORGANIZER:mailto:%s\n";, addr);
-    }
-    if (event->summary) {
-       buf = text_2445_escape(event->summary);
-       g_string_append_printf(retval, "SUMMARY:%s\n", buf);
-       g_free(buf);
-    }
-    if (event->description) {
-       buf = text_2445_escape(event->description);
-       g_string_append_printf(retval, "DESCRIPTION:%s\n", buf);
-       g_free(buf);
-    }
-    if (event->start != (time_t) - 1) {
-       buf = time_t_to_date_time_2445(event->start);
-       g_string_append_printf(retval, "DTSTART:%s\n", buf);
-       g_free(buf);
-    }
-    if (event->end != (time_t) - 1) {
-       buf = time_t_to_date_time_2445(event->end);
-       g_string_append_printf(retval, "DTEND:%s\n", buf);
-       g_free(buf);
-    }
-    retval = g_string_append(retval, "END:VEVENT\nEND:VCALENDAR");
-
-    /* fold */
-    p = 0;
-    eol = strchr(retval->str, '\n');
-    while (eol) {
-       if (eol - (retval->str + p) > 74) {
-           retval = g_string_insert(retval, p + 74, "\n ");
-           p += 76;
-       } else
-           p = eol - retval->str + 1;
-       eol = strchr(retval->str + p, '\n');
-    }
+gchar *
+libbalsa_vevent_duration_str(LibBalsaVEvent *event)
+{
+       gchar *res = NULL;
 
-    /* done */
-    return g_string_free(retval, FALSE);
+       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event), NULL);
+       if (icaldurationtype_is_null_duration(event->duration) == 0) {
+               GString *buffer = g_string_new(NULL);
+
+               if (event->duration.weeks > 0) {
+                       g_string_append_printf(buffer, "%d %s", event->duration.weeks,
+                               ngettext("week", "weeks", event->duration.weeks));
+               }
+               if (event->duration.days > 0) {
+                       if (buffer->len > 0U) {
+                               g_string_append(buffer, ", ");
+                       }
+                       g_string_append_printf(buffer, "%d %s", event->duration.days,
+                               ngettext("day", "days", event->duration.days));
+               }
+               if (event->duration.hours > 0) {
+                       if (buffer->len > 0U) {
+                               g_string_append(buffer, ", ");
+                       }
+                       g_string_append_printf(buffer, "%d %s", event->duration.hours,
+                               ngettext("hour", "hours", event->duration.hours));
+               }
+               if (event->duration.minutes > 0) {
+                       if (buffer->len > 0U) {
+                               g_string_append_c(buffer, ' ');
+                       }
+                       g_string_append_printf(buffer, "%d %s", event->duration.minutes,
+                               ngettext("minute", "minutes", event->duration.minutes));
+               }
+               if (event->duration.seconds > 0) {
+                       if (buffer->len > 0U) {
+                               g_string_append_c(buffer, ' ');
+                       }
+                       g_string_append_printf(buffer, "%d %s", event->duration.seconds,
+                               ngettext("second", "seconds", event->duration.seconds));
+               }
+               res = g_string_free(buffer, FALSE);
+       }
+       return res;
 }
 
 
-/* -- rfc 2445 parser helper functions -- */
+icalproperty_status
+libbalsa_vevent_status(LibBalsaVEvent *event)
+{
+       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event), ICAL_STATUS_NONE);
+       return event->status;
+}
 
-/* convert a rfc 2445 time string into a time_t value */
-// FIXME - what about entries containing a TZID?
-static time_t
-date_time_2445_to_time_t(const gchar *date_time, const gchar *modifier, gboolean *date_only)
-{
-    gint len;
-    time_t the_time = (time_t) (-1);
-
-    g_return_val_if_fail(date_time != NULL, (time_t) (-1));
-    len = strlen(date_time);
-
-    /* must be yyyymmddThhmmssZ? */
-    if (((len == 15) || ((len == 16) && (date_time[15] == 'Z'))) &&
-       (date_time[8] == 'T')) {
-#if GLIB_CHECK_VERSION(2, 56, 0)
-        GDateTime *datetime;
-
-        datetime = g_date_time_new_from_iso8601(date_time, NULL);
-        if (datetime != NULL) {
-            the_time = g_date_time_to_unix(datetime);
-            if (date_only != NULL) {
-                *date_only = FALSE;
-            }
-            g_date_time_unref(datetime);
-        }
-#else /* GLIB_CHECK_VERSION(2, 56, 0) */
-        GTimeVal timeval;
-
-        /* the rfc2445 date-time is a special case of an iso8901 date/time value... */
-        if (g_time_val_from_iso8601(date_time, &timeval)) {
-               the_time = timeval.tv_sec;
-               if (date_only != NULL) {
-                       *date_only = FALSE;
-               }
-        }
-#endif /* GLIB_CHECK_VERSION(2, 56, 0) */
-    } else if ((modifier!= NULL) && (g_ascii_strcasecmp(modifier, "VALUE=DATE") == 0) && (len == 8)) {
-       struct tm tm;
-
-       /* date only (yyyymmdd) */
-       memset(&tm, 0, sizeof(tm));
-       if (strptime(date_time, "%Y%m%d", &tm) != NULL) {
-               the_time = mktime(&tm);
-               if (date_only != NULL) {
-                       *date_only = TRUE;
-               }
-       }
+
+const gchar *
+libbalsa_vevent_status_str(LibBalsaVEvent *event)
+{
+       static struct {
+               icalproperty_status status;
+               gchar *status_str;
+       } status[] = {
+               { ICAL_STATUS_NONE, N_("unknown") },
+               { ICAL_STATUS_TENTATIVE, N_("event is tentative") },
+               { ICAL_STATUS_CONFIRMED, N_("event is definite") },
+               { ICAL_STATUS_CANCELLED, N_("event was cancelled") }
+       };
+       guint n;
+
+       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event), NULL);
+    for (n = 0U; (n < G_N_ELEMENTS(status)) && (status[n].status != event->status); n++) {
+       /* nothing to do */
     }
+    return (n < G_N_ELEMENTS(status)) ? status[n].status_str : status[0].status_str;
+}
 
-    return the_time;
+icalproperty_method
+libbalsa_vcal_method(LibBalsaVCal *vcal)
+{
+    g_return_val_if_fail(LIBBALSA_IS_VCAL(vcal), ICAL_METHOD_NONE);
+    return vcal->method;
 }
 
 
-static gchar *
-time_t_to_date_time_2445(time_t ttime)
+const gchar *
+libbalsa_vcal_method_str(LibBalsaVCal *vcal)
 {
-       gchar *retval = NULL;
-       GDateTime *date_time;
+    static struct {
+       icalproperty_method method;
+               gchar *meth_str;
+    } methods[] = {
+               { ICAL_METHOD_NONE, N_("unknown") },
+               { ICAL_METHOD_PUBLISH, N_("Event Notification") },
+               { ICAL_METHOD_REQUEST, N_("Event Request") },
+               { ICAL_METHOD_REPLY, N_("Reply to Event Request") },
+               { ICAL_METHOD_CANCEL, N_("Event Cancellation") }
+       };
+    guint n;
 
-       date_time = g_date_time_new_from_unix_utc(ttime);
-       if (date_time != NULL) {
-               retval = g_date_time_format(date_time, "%Y%m%dT%H%M%SZ");
-               g_date_time_unref(date_time);
-       }
-       return retval;
+    for (n = 0U; (n < G_N_ELEMENTS(methods)) && (methods[n].method != vcal->method); n++) {
+       /* nothing to do */
+    }
+    return (n < G_N_ELEMENTS(methods)) ? methods[n].meth_str : methods[0].meth_str;
 }
 
 
-/* unescape a text from rfc 2445 format to a plain string */
-static gchar *
-text_2445_unescape(const gchar * text)
-{
-    gchar *retval;
-    gchar *p;
-
-    g_return_val_if_fail(text != NULL, NULL);
-    retval = g_strdup(text);
-    p = strchr(retval, '\\');
-    while (p) {
-       if (p[1] == 'n' || p[1] == 'N')
-           p[1] = '\n';
-       memmove(p, p + 1, strlen(p + 1) + 1);
-       p = strchr(retval, '\\');
+/* return a rfc 2445 participant status as human-readable string */
+const gchar *
+libbalsa_vcal_part_stat_to_str(icalparameter_partstat pstat)
+{
+    static struct {
+       icalparameter_partstat partstat;
+       gchar *partstat_str;
+    } partstats[] = {
+       { ICAL_PARTSTAT_NONE, N_("unknown") },
+       { ICAL_PARTSTAT_NEEDSACTION, N_("needs action") },
+       { ICAL_PARTSTAT_ACCEPTED, N_("accepted") },
+               { ICAL_PARTSTAT_DECLINED, N_("declined") },
+               { ICAL_PARTSTAT_TENTATIVE, N_("tentatively accepted") },
+               { ICAL_PARTSTAT_DELEGATED, N_("delegated") }
+    };
+    guint n;
+
+    for (n = 0U; (n < G_N_ELEMENTS(partstats)) && (partstats[n].partstat != pstat); n++) {
+       /* nothing to do */
     }
-    return retval;
+    return (n < G_N_ELEMENTS(partstats)) ? partstats[n].partstat_str : partstats[0].partstat_str;
 }
 
 
-/* unescape a text from rfc 2445 format to a plain string */
-static gchar *
-text_2445_escape(const gchar * text)
+guint
+libbalsa_vcal_vevent_count(LibBalsaVCal *vcal)
 {
-    GString *retval;
-    static const gchar *do_escape = "\\;,\n";
-    const gchar *esc_p;
-
-    g_return_val_if_fail(text != NULL, NULL);
-    retval = g_string_new(text);
-    for (esc_p = do_escape; *esc_p; esc_p++) {
-       gchar *enc_p = strchr(retval->str, *esc_p);
+       g_return_val_if_fail(LIBBALSA_IS_VCAL(vcal), 0U);
+       return g_list_length(vcal->vevent);
+}
 
-       while (enc_p) {
-           gssize inspos = enc_p - retval->str;
 
-           if (*enc_p == '\n')
-               *enc_p = 'n';
-           retval = g_string_insert_c(retval, inspos, '\\');
-           enc_p = strchr(retval->str + inspos + 2, *esc_p);
-       }
-    }
-    return g_string_free(retval, FALSE);
+LibBalsaVEvent *
+libbalsa_vcal_vevent(LibBalsaVCal *vcal, guint nth_event)
+{
+       g_return_val_if_fail(LIBBALSA_IS_VCAL(vcal) && (nth_event < g_list_length(vcal->vevent)), NULL);
+       return (LibBalsaVEvent *) g_list_nth_data(vcal->vevent, nth_event);
 }
 
 
-/* extract a rfc 2445 mailto address and into a LibBalsaAddress and add the
- * extra information (rfc 2445 attributes) as data to the returned object. */
-static LibBalsaAddress *
-cal_address_2445_to_lbaddress(const gchar * uri, gchar ** attributes,
-                             gboolean is_organizer)
+/* -- recurrence strings --
+ * According to RFC 5545, Sect. 3.3.1, these rules can be extremely complex.  The following functions 
basically implement the same
+ * features als Thunderbird's "Lightning" extension, which should be fine for the majority of cases in the 
wild.  However, there
+ * /may/ (and will) be cases where these function just fail... */
+
+static inline gboolean
+ical_ar_empty(const short *array)
 {
-    LibBalsaAddress *retval;
-    LibBalsaVCalInfo *info;
+       return array[0] == ICAL_RECURRENCE_ARRAY_MAX;
+}
 
-    /* must be a mailto: uri */
-    if (g_ascii_strncasecmp("mailto:";, uri, 7))
-       return NULL;
-
-    retval = libbalsa_address_new();
-    libbalsa_address_append_addr(retval, uri + 7);
-
-    info = g_new0(LibBalsaVCalInfo, 1);
-    g_object_set_data_full(G_OBJECT(retval), RFC2445_INFO, info, g_free);
-
-    if (!is_organizer)
-        info->role = VCAL_ROLE_REQ_PART;
-
-    if (attributes) {
-       int n;
-       gchar *the_attr;
-
-       /* scan attributes for extra information */
-       for (n = 0; (the_attr = attributes[n]); n++) {
-           if (!g_ascii_strncasecmp(the_attr, "CN=", 3))
-               libbalsa_address_set_full_name(retval, the_attr + 3);
-           else if (!g_ascii_strncasecmp(the_attr, "ROLE=", 5))
-               info->role = vcal_str_to_role(the_attr + 5);
-           else if (!g_ascii_strncasecmp(the_attr, "PARTSTAT=", 9))
-               info->part_stat = vcal_str_to_part_stat(the_attr + 9);
-           else if (!g_ascii_strncasecmp(the_attr, "RSVP=", 5))
-               info->rsvp = g_ascii_strcasecmp(the_attr + 5, "TRUE") == 0;
-       }
-    }
 
-    return retval;
+static inline const gchar *
+day_name(gint day)
+{
+       static const gchar * day_names[7] =
+               { N_("Sunday"), N_("Monday"), N_("Tuesday"), N_("Wednesday"), N_("Thursday"), N_("Friday"), 
N_("Saturday") };
+
+       return ((day >= 0) && (day < 7)) ? day_names[day] : "???";
 }
 
 
-/* -- conversion helpers string <--> enumeration -- */
+/** \brief Check for "BYDAY" items in any order
+ *
+ * \param rrule recurrence rule
+ * \param want_days bit mask of expected items in "BYDAY" (bit 1 = Sunday, bit 7 = Saturday)
+ * \return TRUE if the "BYDAY" item contains exactly all requested week days in any order
+ */
+static gboolean
+ical_check_bydays(const struct icalrecurrencetype *rrule, guint want_days)
+{
+       guint have_days = 0U;
+       guint n;
 
-/* convert the passed method string into the enumeration */
-static LibBalsaVCalMethod
-vcal_str_to_method(const gchar * method)
+       for (n = 0U; (n < ICAL_BY_DAY_SIZE) && (rrule->by_day[n] != ICAL_RECURRENCE_ARRAY_MAX); n++) {
+               have_days |= (1U << rrule->by_day[n]);
+       }
+       return want_days == have_days;
+}
+
+
+static GString *
+vevent_recurrence_daily(const struct icalrecurrencetype *rrule)
 {
-    static struct {
-       gchar *meth_id;
-       LibBalsaVCalMethod meth;
-    } meth_list[] = {
-       { "PUBLISH", ITIP_PUBLISH },
-        { "REQUEST", ITIP_REQUEST },
-        { "REPLY", ITIP_REPLY },
-        { "CANCEL", ITIP_CANCEL },
-        { NULL, ITIP_UNKNOWN}
-    };
-    gint n;
+       GString *result = g_string_new(NULL);
+
+       if (ical_check_bydays(rrule, 0x7cU)) {
+               g_string_append(result, _("every weekday"));
+       } else if (rrule->interval == 1) {
+               g_string_append(result, _("every day"));
+       } else {
+               g_string_append_printf(result, _("every %d days"), rrule->interval);
+       }
+       return result;
+}
 
-    if (!method)
-        return ITIP_UNKNOWN;
-    for (n = 0;
-        meth_list[n].meth_id
-        && g_ascii_strcasecmp(method, meth_list[n].meth_id); n++);
-    return meth_list[n].meth;
+
+static GString *
+vevent_recurrence_weekly(const struct icalrecurrencetype *rrule)
+{
+       GString *result = g_string_new(NULL);
+
+       if (!ical_ar_empty(rrule->by_day)) {
+               gint n;
+
+               if (rrule->interval == 1) {
+                       g_string_append(result, _("every "));
+               } else {
+                       g_string_append_printf(result, _("every %d weeks on "), rrule->interval);
+               }
+               g_string_append(result, day_name(rrule->by_day[0] - 1));
+               for (n = 1; (n < ICAL_BY_DAY_SIZE) && (rrule->by_day[n] != ICAL_RECURRENCE_ARRAY_MAX); n++) {
+                       if ((n < (ICAL_BY_DAY_SIZE - 1)) && (rrule->by_day[n + 1] != 
ICAL_RECURRENCE_ARRAY_MAX)) {
+                               g_string_append_printf(result, ", %s", day_name(rrule->by_day[n] - 1));
+                       } else {
+                               g_string_append_printf(result, _(" and %s"), day_name(rrule->by_day[n] - 1));
+                       }
+               }
+       } else {
+               if (rrule->interval == 1) {
+                       g_string_append(result, _("every week"));
+               } else {
+                       g_string_append_printf(result, _("every %d weeks"), rrule->interval);
+               }
+       }
+       return result;
 }
 
 
-LibBalsaVCalMethod
-libbalsa_vcal_method(LibBalsaVCal *vcal)
+static void
+day_ordinal_append(GString *buffer, int day, const gchar *last_append, const gchar *ordinal_append)
 {
-    g_return_val_if_fail(LIBBALSA_IS_VCAL(vcal) &&
-       (vcal->method >= ITIP_UNKNOWN) && (vcal->method <= ITIP_CANCEL), ITIP_UNKNOWN);
-    return vcal->method;
+       if (day == -1) {
+               g_string_append(buffer, _("the last"));
+               if (last_append != NULL) {
+                       g_string_append_printf(buffer, " %s", last_append);
+               }
+       } else {
+               switch (day % 10) {
+               case 1:
+                       g_string_append_printf(buffer, _("%dst"), day);
+                       break;
+               case 2:
+                       g_string_append_printf(buffer, _("%dnd"), day);
+                       break;
+               case 3:
+                       g_string_append_printf(buffer, _("%drd"), day);
+                       break;
+               default:
+                       g_string_append_printf(buffer, _("%dth"), day);
+               }
+               if (ordinal_append != NULL) {
+                       g_string_append_printf(buffer, " %s", ordinal_append);
+               }
+       }
 }
 
-/* return a rfc 2445 method as human-readable string */
-const gchar *
-libbalsa_vcal_method_str(LibBalsaVCal *vcal)
+
+static GString *
+vevent_recurrence_monthly(const struct icalrecurrencetype *rrule, const icaltimetype *start)
 {
-       static gchar *methods[] = {
-               N_("unknown"),
-               N_("Event Notification"),
-               N_("Event Request"),
-               N_("Reply to Event Request"),
-               N_("Event Cancellation"),
-       };
+       GString *result = g_string_new(NULL);
+
+       if (!ical_ar_empty(rrule->by_day)) {
+               /* we have a "BYDAY" rule */
+               if (ical_check_bydays(rrule, 0xfeU)) {
+                       if (rrule->interval == 1) {
+                               g_string_append(result, _("every day of every month"));
+                       } else {
+                               g_string_append_printf(result, _("every day of the month every %d months"), 
rrule->interval);
+                       }
+                       return result;          /* eject here so we don't append the month interval again... 
*/
+               } else {
+                       guint n;
+                       guint every_mask = 0U;
+                       GList *days = NULL;
+                       GList *p;
+
+                       /* collect all days repeating every week */
+                       for (n = 0U; (n < ICAL_BY_DAY_SIZE) && (rrule->by_day[n] != 
ICAL_RECURRENCE_ARRAY_MAX); n++) {
+                               int day_pos;
+
+                               day_pos = icalrecurrencetype_day_position(rrule->by_day[n]);
+                               if ((day_pos < -1) || (day_pos > 5)) {
+                                       return g_string_assign(result, _("rule too complex"));
+                               } else if (day_pos == 0) {
+                                       int day_of_week;
+
+                                       day_of_week = icalrecurrencetype_day_day_of_week(rrule->by_day[n]);
+                                       every_mask |= 1U << day_of_week;
+                                       days = g_list_append(days, g_strdup_printf(_("every %s"), 
day_name(day_of_week - 1)));
+                               } else {
+                                       /* handled below... */
+                               }
+                       }
+
+                       /* collect specific ones, but avoid something like "Monday and the last Monday" */
+                       for (n = 0U; (n < ICAL_BY_DAY_SIZE) && (rrule->by_day[n] != 
ICAL_RECURRENCE_ARRAY_MAX); n++) {
+                               int day_of_week;
+
+                               day_of_week = icalrecurrencetype_day_day_of_week(rrule->by_day[n]);
+                               if ((every_mask & (1U << day_of_week)) == 0U) {
+                                       int day_pos;
+                                       GString *buffer = g_string_new(NULL);
+
+                                       day_pos = icalrecurrencetype_day_position(rrule->by_day[n]);
+                                       day_ordinal_append(buffer, day_pos, day_name(day_of_week - 1), 
day_name(day_of_week - 1));
+                                       days = g_list_append(days, g_string_free(buffer, FALSE));
+                               }
+                       }
+
+                       /* glue the string together */
+                       g_string_append(result, (const gchar *) days->data);
+                       for (p = days->next; p != NULL; p = p->next) {
+                               if (p->next != NULL) {
+                                       g_string_append_printf(result, ", %s", (const gchar *) p->data);
+                               } else {
+                                       g_string_append_printf(result, _(" and %s"), (const gchar *) p->data);
+                               }
+                       }
+                       g_list_free_full(days, g_free);
+               }
+       } else if (!ical_ar_empty(rrule->by_month_day)) {
+               /* we have a "BYMONTHDAY" rule */
+               guint n;
+
+               for (n = 0; (n < ICAL_BY_MONTHDAY_SIZE) && (rrule->by_month_day[n] != 
ICAL_RECURRENCE_ARRAY_MAX); n++) {
+                       if (rrule->by_month_day[n] < -1) {
+                               return g_string_assign(result, _("rule too complex"));
+                       } else {
+                               if (n > 0) {
+                                       if (rrule->by_month_day[n + 1] != ICAL_RECURRENCE_ARRAY_MAX) {
+                                               g_string_append(result, ", ");
+                                       } else {
+                                               g_string_append(result, _(" and "));
+                                       }
+                               }
+                               day_ordinal_append(result, rrule->by_month_day[n], _("day"), NULL);
+                       }
+               }
+       } else {
+               day_ordinal_append(result, start->day, _("day"), NULL);
+       }
 
-    g_return_val_if_fail(LIBBALSA_IS_VCAL(vcal) &&
-       (vcal->method >= ITIP_UNKNOWN) && (vcal->method <= ITIP_CANCEL), NULL);
-    return methods[(int) vcal->method];
+       if (rrule->interval == 1) {
+               g_string_append(result, _(" of every month"));
+       } else {
+               g_string_append_printf(result, _(" of every %d months"), rrule->interval);
+       }
+
+       return result;
 }
 
 
-/* return a rfc 2445 role as human-readable string */
-static const gchar *
-vcal_role_to_str(LibBalsaVCalRole role)
-{
-    static gchar *roles[] = {
-        N_("unknown"),
-       N_("chair"),
-       N_("required participant"),
-       N_("optional participant"),
-       N_("non-participant, information only")
-    };
+static GString *
+vevent_recurrence_yearly(const struct icalrecurrencetype *rrule, const icaltimetype *start)
+{
+       static const gchar *mon_name[12] =
+               { N_("January"), N_("February"), N_("March"), N_("April"), N_("May"), N_("June"),
+                 N_("July"), N_("August"), N_("September"), N_("October"), N_("November"), N_("December") };
+       GString *result = g_string_new(NULL);
+
+       /* rules which are too complex for Lightning, so ignore them here, too... */
+       if ((rrule->by_month[1] != ICAL_RECURRENCE_ARRAY_MAX) || (rrule->by_month_day[1] != 
ICAL_RECURRENCE_ARRAY_MAX) ||
+               (rrule->by_month_day[0] < -1)) {
+               return g_string_assign(result, _("rule too complex"));
+       }
 
-    g_return_val_if_fail(role >= VCAL_ROLE_UNKNOWN
-                        && role <= VCAL_ROLE_NON_PART, NULL);
-    return roles[(int) role];
+       if (ical_ar_empty(rrule->by_day)) {
+        /* RRULE:FREQ=YEARLY;BYMONTH=x;BYMONTHDAY=y.
+         * RRULE:FREQ=YEARLY;BYMONTHDAY=x (takes the month from the start date)
+         * RRULE:FREQ=YEARLY;BYMONTH=x (takes the day from the start date)
+         * RRULE:FREQ=YEARLY (takes month and day from the start date) */
+               const gchar *month;
+               GString *day = g_string_new(NULL);
+
+               if (ical_ar_empty(rrule->by_month)) {
+                       month = mon_name[start->month - 1];
+               } else {
+                       month = mon_name[rrule->by_month[0] - 1];
+               }
+               if (ical_ar_empty(rrule->by_month_day)) {
+                       day_ordinal_append(day, start->day, NULL, NULL);
+               } else {
+                       day_ordinal_append(day, rrule->by_month_day[0], _("day"), NULL);
+               }
+               if (rrule->interval == 1) {
+                       g_string_append_printf(result, _("every %s %s"), month, day->str);
+               } else {
+                       g_string_append_printf(result, _("every %d years on %s %s"), rrule->interval, month, 
day->str);
+               }
+               g_string_free(day, TRUE);
+       } else if (!ical_ar_empty(rrule->by_month) && !ical_ar_empty(rrule->by_day)) {
+               /* RRULE:FREQ=YEARLY;BYMONTH=x;BYDAY=y1,y2,... */
+               const gchar *month = mon_name[rrule->by_month[0] - 1];
+
+               if (ical_check_bydays(rrule, 0x7cU)) {
+                       /* every day of the month */
+                       if (rrule->interval == 1) {
+                               g_string_append_printf(result, _("every day of %s"), month);
+                       } else {
+                               g_string_append_printf(result, _("every %d years every day of %s"), 
rrule->interval, month);
+                       }
+               } else if (rrule->by_day[1] == ICAL_RECURRENCE_ARRAY_MAX) {
+                       int day_pos;
+                       int day_of_week;
+
+                       day_pos = icalrecurrencetype_day_position(rrule->by_day[0]);
+                       day_of_week = icalrecurrencetype_day_day_of_week(rrule->by_day[0]);
+                       if (day_pos == 0) {
+                               if (rrule->interval == 1) {
+                                       g_string_append_printf(result, _("every %s of %s"), 
day_name(day_of_week - 1), month);
+                               } else {
+                                       g_string_append_printf(result, _("every %d years on every %s of %s"), 
rrule->interval,
+                                               day_name(day_of_week - 1), month);
+                               }
+                       } else if ((day_pos >= -1) && (day_pos <= 5)) {
+                               GString *day = g_string_new(NULL);
+
+                               day_ordinal_append(day, day_pos, day_name(day_of_week - 1), 
day_name(day_of_week - 1));
+                               if (rrule->interval == 1) {
+                                       g_string_append_printf(result, _("the %s of every %s"), day->str, 
month);
+                               } else {
+                                       g_string_append_printf(result, _("every %d years on the %s of %s"), 
rrule->interval, day->str, month);
+                               }
+                               g_string_free(day, TRUE);
+                       } else {
+                               g_string_assign(result, _("rule too complex"));
+                       }
+               } else {
+                       /* currently we don't support yearly rules with more than one BYDAY element or 
exactly 7 elements with all the weekdays
+                        * (the "every day" case) */
+                       g_string_assign(result, _("rule too complex"));
+               }
+       } else {
+               return g_string_assign(result, _("rule too complex"));
+       }
+
+       return result;
 }
 
 
-/* convert the passed role string into the enumeration */
-static LibBalsaVCalRole
-vcal_str_to_role(const gchar * role)
+gchar *
+libbalsa_vevent_recurrence_str(LibBalsaVEvent *event, const gchar *format_str)
 {
-    static struct {
-       gchar *role_id;
-       LibBalsaVCalRole role;
-    } role_list[] = {
-       { "CHAIR", VCAL_ROLE_CHAIR },
-        { "REQ-PARTICIPANT", VCAL_ROLE_REQ_PART },
-        { "OPT-PARTICIPANT", VCAL_ROLE_OPT_PART },
-        { "NON-PARTICIPANT", VCAL_ROLE_NON_PART },
-        { NULL, VCAL_ROLE_UNKNOWN}
-    };
-    gint n;
+       gchar *result = NULL;
+
+       g_return_val_if_fail(LIBBALSA_IS_VEVENT(event), NULL);
 
-    if (!role)
-        return VCAL_ROLE_UNKNOWN;
-    for (n = 0;
-        role_list[n].role_id
-        && g_ascii_strcasecmp(role, role_list[n].role_id); n++);
-    return role_list[n].role;
+       if ((event->rrule.freq >= ICAL_SECONDLY_RECURRENCE) && (event->rrule.freq <= ICAL_YEARLY_RECURRENCE)) 
{
+               GString *buffer;
+
+               switch (event->rrule.freq) {
+               case ICAL_SECONDLY_RECURRENCE:
+                       buffer = g_string_new(_("secondly"));
+                       break;
+               case ICAL_MINUTELY_RECURRENCE:
+                       buffer = g_string_new(_("minutely"));
+                       break;
+               case ICAL_HOURLY_RECURRENCE:
+                       buffer = g_string_new(_("hourly"));
+                       break;
+               case ICAL_DAILY_RECURRENCE:
+               buffer = vevent_recurrence_daily(&event->rrule);
+                       break;
+               case ICAL_WEEKLY_RECURRENCE:
+               buffer = vevent_recurrence_weekly(&event->rrule);
+                       break;
+               case ICAL_MONTHLY_RECURRENCE:
+               buffer = vevent_recurrence_monthly(&event->rrule, &event->start);
+                       break;
+               case ICAL_YEARLY_RECURRENCE:
+                       buffer = vevent_recurrence_yearly(&event->rrule, &event->start);
+                       break;
+               default:
+                       g_assert_not_reached();
+               }
+
+               /* end timestamp or count */
+               if (event->rrule.count > 0) {
+                       g_string_append_printf(buffer, ", %d ", event->rrule.count);
+                       g_string_append(buffer, ngettext(_("occurrence"), _("occurrences"), 
event->rrule.count));
+               } else if (icaltime_is_null_time(event->rrule.until) == 0) {
+                       gchar *timestr;
+
+                       timestr = icaltime_str(event->rrule.until, format_str);
+                       g_string_append_printf(buffer, _(" until %s"), timestr);
+                       g_free(timestr);
+               } else {
+                       /* nothing to do */
+               }
+
+               result = g_string_free(buffer, FALSE);
+       }
+       return result;
 }
 
 
-/* return a rfc 2445 participant status as human-readable string */
-const gchar *
-libbalsa_vcal_part_stat_to_str(LibBalsaVCalPartStat pstat)
+/* return a new buffer containing a proper reply to an event for a new
+ * participant status
+ *
+ * According to RFC 5546, Sect. 3.2.3., the REPLY shall include the following components and properties:
+ * METHOD: MUST be REPLY
+ * VEVENT: ATTENDEE, DTSTAMP, ORGANIZER, RECURRENCE-ID (if present in the request), UID, SEQUENCE (if 
present in the request)
+ */
+gchar *
+libbalsa_vevent_reply(const LibBalsaVEvent   *event,
+                                         const gchar            *sender,
+                                         icalparameter_partstat  new_stat)
 {
-    g_return_val_if_fail(pstat >= VCAL_PSTAT_UNKNOWN
-                        && pstat <= VCAL_PSTAT_IN_PROCESS, NULL);
-    return pstats[(int) pstat].hr_text;
+       icalcomponent *reply;
+       icalcomponent *ev_data;
+       icalproperty *prop;
+       gchar *buf;
+       gchar *reply_str;
+
+    /* check for allowed new state and sender */
+    g_return_val_if_fail((new_stat == ICAL_PARTSTAT_ACCEPTED ||
+                                                 new_stat == ICAL_PARTSTAT_DECLINED ||
+                                                 new_stat == ICAL_PARTSTAT_TENTATIVE) &&
+                        sender != NULL, NULL);
+
+    reply = icalcomponent_new(ICAL_VCALENDAR_COMPONENT);
+
+    icalcomponent_add_property(reply, icalproperty_new_version("2.0"));
+    icalcomponent_add_property(reply, icalproperty_new_prodid("-//GNOME//Balsa " BALSA_VERSION "//EN"));
+    icalcomponent_add_property(reply, icalproperty_new_method(ICAL_METHOD_REPLY));
 
+    /* create the VEVENT */
+    ev_data = icalcomponent_new(ICAL_VEVENT_COMPONENT);
+    icalcomponent_add_component(reply, ev_data);
+
+    /* add ATTENDEE */
+    buf = g_strconcat("mailto:";, sender, NULL);
+    prop = icalproperty_new_attendee(buf);
+    g_free(buf);
+    icalproperty_add_parameter(prop, icalparameter_new_partstat(new_stat));
+    icalcomponent_add_property(ev_data, prop);
+
+    /* add DTSTAMP */
+    icalcomponent_add_property(ev_data, icalproperty_new_dtstamp(icaltime_current_time_with_zone(NULL)));
+
+    /* add ORGANIZER - /should/ be present */
+    if (event->organizer != NULL) {
+        const gchar *addr = libbalsa_address_get_addr(event->organizer);
+
+        if (addr != NULL) {
+               icalcomponent_add_property(ev_data, icalproperty_new_organizer(addr));
+        }
+    }
+
+    /* add RECURRENCE-ID (if present) */
+    if (icaltime_is_null_time(event->recurrence_id) == 0) {
+       icalcomponent_add_property(ev_data, icalproperty_new_recurrenceid(event->recurrence_id));
+    }
+
+    /* add UID - /should/ be present */
+    if (event->uid != NULL) {
+       icalcomponent_add_property(ev_data, icalproperty_new_uid(event->uid));
+    }
+
+    /* add SEQUENCE (if present) */
+    if (event->sequence > 0) {
+       icalcomponent_add_property(ev_data, icalproperty_new_sequence(event->sequence));
+    }
+
+    reply_str = icalcomponent_as_ical_string(reply);
+    icalcomponent_free(reply);
+    return reply_str;
 }
 
-guint
-libbalsa_vcal_vevents(LibBalsaVCal *vcal)
+
+/* -- rfc 2445 parser helper functions -- */
+
+static LibBalsaVCal *
+vcalendar_extract(const gchar *vcal_buf)
 {
-       g_return_val_if_fail(LIBBALSA_IS_VCAL(vcal), 0U);
-       return g_list_length(vcal->vevent);
+    LibBalsaVCal *retval = NULL;
+       icalcomponent *component;
+
+       component = icalparser_parse_string(vcal_buf);
+       if (component == NULL) {
+               return NULL;
+       }
+
+       /* verify - icalrestriction_check() seems to produce False Positives for some Thunderbird events, so 
do *not* use it here */
+       if (icalcomponent_isa(component) == ICAL_VCALENDAR_COMPONENT) {
+               icalcomponent *item;
+
+               retval = LIBBALSA_VCAL(g_object_new(LIBBALSA_TYPE_VCAL, NULL));
+               retval->method = icalcomponent_get_method(component);
+               retval->vcalendar = component;
+
+               /* loop over all VEVENT items (if any) */
+               for (item = icalcomponent_get_first_component(component, ICAL_VEVENT_COMPONENT);
+                        item != NULL;
+                        item = icalcomponent_get_next_component(component, ICAL_VEVENT_COMPONENT)) {
+                       LibBalsaVEvent *event;
+                       icalproperty *prop;
+
+                       event = LIBBALSA_VEVENT(g_object_new(LIBBALSA_TYPE_VEVENT, NULL));
+                       retval->vevent = g_list_append(retval->vevent, event);
+
+                       /* extract properties - DTSTAMP, DTSTART, DTEND
+                        * note: there is no need to get the DURATION, as libical calculates the proper DTEND 
if necessary. However, in
+                        * particular for all-day events, the duration is sometimes easier to understand than 
the (exclusive) end. */
+                       event->stamp = icalcomponent_get_dtstamp(item);
+                       event->start = icalcomponent_get_dtstart(item);
+                       event->end = icalcomponent_get_dtend(item);
+                       event->duration = icalcomponent_get_duration(item);
+                       if (icaldurationtype_is_bad_duration(event->duration)) {
+                               event->duration = icaldurationtype_null_duration();
+                       }
+
+                       /* RECURRENCE-ID */
+                       event->recurrence_id = icalcomponent_get_recurrenceid(item);
+
+                       /* STATUS */
+                       event->status = icalcomponent_get_status(item);
+                       if (event->status == (icalproperty_status) 0) {
+                               event->status = ICAL_STATUS_NONE;
+                       }
+
+                       /* RRULE (only one is allowed, see RFC 5546, Sect. 3.2.2) */
+                       prop = icalcomponent_get_first_property(item, ICAL_RRULE_PROPERTY);
+                       if (prop != NULL) {
+                               event->rrule = icalproperty_get_rrule(prop);
+                       }
+
+                       /* UID, SUMMARY, LOCATION, DESCRIPTION */
+                       event->uid = g_strdup(icalcomponent_get_uid(item));
+                       event->summary = g_strdup(icalcomponent_get_summary(item));
+                       event->location = g_strdup(icalcomponent_get_location(item));
+                       event->description = g_strdup(icalcomponent_get_description(item));
+
+                       /* ORGANIZER */
+                       prop = icalcomponent_get_first_property(item, ICAL_ORGANIZER_PROPERTY);
+                       if (prop != NULL) {
+                               event->organizer = cal_address_5545_to_lbaddress(prop, TRUE);
+                       }
+
+                       /* ATTENDEE */
+                       for (prop = icalcomponent_get_first_property(item, ICAL_ATTENDEE_PROPERTY);
+                                prop != NULL;
+                                prop = icalcomponent_get_next_property(item, ICAL_ATTENDEE_PROPERTY)) {
+                               LibBalsaAddress *attendee;
+
+                               attendee = cal_address_5545_to_lbaddress(prop, FALSE);
+                               if (attendee != NULL) {
+                                       event->attendee = g_list_prepend(event->attendee, attendee);
+                               }
+                       }
+
+                   /* SEQUENCE (if present) */
+                       event->sequence = icalcomponent_get_sequence(item);
+
+                       /* CATEGORIES */
+                       for (prop = icalcomponent_get_first_property(item, ICAL_CATEGORIES_PROPERTY);
+                                prop != NULL;
+                                prop = icalcomponent_get_next_property(item, ICAL_CATEGORIES_PROPERTY)) {
+                               event->categories = g_list_append(event->categories, 
g_strdup(icalproperty_get_categories(prop)));
+                       }
+               }
+       }
+
+       return retval;
 }
 
-LibBalsaVEvent *
-libbalsa_vcal_vevent(LibBalsaVCal *vcal, guint nth_event)
+
+/* extract a rfc 5545 mailto address and into a LibBalsaAddress and add the
+ * rfc 5545 attributes as data to the returned object. */
+static LibBalsaAddress *
+cal_address_5545_to_lbaddress(icalproperty *prop, gboolean is_organizer)
 {
-       g_return_val_if_fail(LIBBALSA_IS_VCAL(vcal) && (nth_event < g_list_length(vcal->vevent)), NULL);
-       return (LibBalsaVEvent *) g_list_nth_data(vcal->vevent, nth_event);
+       const gchar *uri;
+    LibBalsaAddress *retval = NULL;
+
+    if (is_organizer) {
+       uri = icalproperty_get_organizer(prop);
+    } else {
+       uri = icalproperty_get_attendee(prop);
+    }
+
+    /* must be a mailto: uri */
+    if (g_ascii_strncasecmp("mailto:";, uri, 7) == 0) {
+        LibBalsaVCalInfo *info;
+               icalparameter *param;
+
+        retval = libbalsa_address_new();
+        libbalsa_address_append_addr(retval, &uri[7]);
+
+        info = g_new0(LibBalsaVCalInfo, 1);
+        g_object_set_data_full(G_OBJECT(retval), RFC2445_INFO, info, g_free);
+        if (!is_organizer) {
+            info->role = ICAL_ROLE_REQPARTICIPANT;
+        } else {
+               info->role = ICAL_ROLE_NONE;
+        }
+        info->part_stat = ICAL_PARTSTAT_NONE;
+        info->rsvp = ICAL_RSVP_NONE;
+
+               for (param = icalproperty_get_first_parameter(prop, ICAL_ANY_PARAMETER);
+                        param != NULL;
+                        param = icalproperty_get_next_parameter(prop, ICAL_ANY_PARAMETER)) {
+                       switch (icalparameter_isa(param)) {
+                       case ICAL_CN_PARAMETER:
+                               libbalsa_address_set_full_name(retval, icalparameter_get_cn(param));
+                               break;
+                       case ICAL_PARTSTAT_PARAMETER:
+                               info->part_stat = icalparameter_get_partstat(param);
+                               break;
+                       case ICAL_ROLE_PARAMETER:
+                               info->role = icalparameter_get_role(param);
+                               break;
+                       case ICAL_RSVP_PARAMETER:
+                               info->rsvp = icalparameter_get_rsvp(param);
+                               break;
+                       default:
+                               /* nothing to do, make gcc happy */
+                               break;
+                       }
+               }
+    }
+
+    return retval;
 }
 
-/* convert the passed participant status string into the enumeration */
-static LibBalsaVCalPartStat
-vcal_str_to_part_stat(const gchar * pstat)
+
+/* return a rfc 2445 role as human-readable string */
+static const gchar *
+vcal_role_to_str(icalparameter_role role)
 {
     static struct {
-       gchar *pstat_id;
-       LibBalsaVCalPartStat pstat;
-    } pstat_list[] = {
-       { "NEEDS-ACTION", VCAL_PSTAT_NEEDS_ACTION },
-        { "ACCEPTED", VCAL_PSTAT_ACCEPTED},
-        { "DECLINED", VCAL_PSTAT_DECLINED },
-        { "TENTATIVE", VCAL_PSTAT_TENTATIVE },
-        { "DELEGATED", VCAL_PSTAT_DELEGATED },
-        { "COMPLETED", VCAL_PSTAT_COMPLETED },
-        { "IN-PROCESS", VCAL_PSTAT_IN_PROCESS },
-        { NULL, VCAL_PSTAT_UNKNOWN }
+       icalparameter_role role;
+       gchar *role_str;
+    } roles[] = {
+       { ICAL_ROLE_NONE, N_("unknown") },
+       { ICAL_ROLE_CHAIR, N_("chair") },
+               { ICAL_ROLE_REQPARTICIPANT, N_("required participant") },
+               { ICAL_ROLE_OPTPARTICIPANT, N_("optional participant") },
+               { ICAL_ROLE_NONPARTICIPANT, N_("non-participant, information only") }
     };
-    gint n;
-
-    if (!pstat)
-        return VCAL_PSTAT_UNKNOWN;
-    for (n = 0;
-        pstat_list[n].pstat_id
-        && g_ascii_strcasecmp(pstat, pstat_list[n].pstat_id); n++);
-    return pstat_list[n].pstat;
+    guint n;
+
+    for (n = 0U; (n < G_N_ELEMENTS(roles)) && (roles[n].role != role); n++) {
+       /* nothing to do */
+    }
+    return (n < G_N_ELEMENTS(roles)) ? roles[n].role_str : roles[0].role_str;
+}
+
+
+static gchar *
+icaltime_str(icaltimetype ical_time, const gchar *format_str)
+{
+       gchar *result = NULL;
+
+       if (icaltime_is_null_time(ical_time) == 0) {
+               time_t unix_time;
+
+               unix_time = icaltime_as_timet_with_zone(ical_time, ical_time.zone);
+               if (ical_time.is_date == 0) {
+                       result = libbalsa_date_to_utf8(unix_time, format_str);
+               } else {
+                       result = libbalsa_date_to_utf8(unix_time, "%x");
+               }
+       }
+
+       return result;
 }
diff --git a/libbalsa/rfc2445.h b/libbalsa/rfc2445.h
index d9676e44a..97b7022ca 100644
--- a/libbalsa/rfc2445.h
+++ b/libbalsa/rfc2445.h
@@ -1,7 +1,7 @@
 /* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
 /*
- * VCalendar (RFC 2445) stuff
- * Copyright (C) 2009-2019 Albrecht Dreß <albrecht dress arcor de>
+ * VCalendar (RFC 5545 and 5546) stuff
+ * Copyright (C) 2009-2020 Albrecht Dreß <albrecht dress arcor de>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,6 +23,7 @@
 
 #include <glib.h>
 #include <glib-object.h>
+#include <libical/ical.h>
 #include <time.h>
 #include "body.h"
 #include "address.h"
@@ -40,28 +41,6 @@ G_DECLARE_FINAL_TYPE(LibBalsaVCal, libbalsa_vcal, LIBBALSA, VCAL, GObject)
 G_DECLARE_FINAL_TYPE(LibBalsaVEvent, libbalsa_vevent, LIBBALSA, VEVENT, GObject)
 
 
-/* methods as defined by RFC 2446 */
-typedef enum {
-    ITIP_UNKNOWN = 0,
-    ITIP_PUBLISH,
-    ITIP_REQUEST,
-    ITIP_REPLY,
-    ITIP_CANCEL
-} LibBalsaVCalMethod;
-
-/* participation status as defined by RFC 2445
- * (note: includes constants for VTODO) */
-typedef enum {
-    VCAL_PSTAT_UNKNOWN = 0,
-    VCAL_PSTAT_NEEDS_ACTION,
-    VCAL_PSTAT_ACCEPTED,
-    VCAL_PSTAT_DECLINED,
-    VCAL_PSTAT_TENTATIVE,
-    VCAL_PSTAT_DELEGATED,
-    VCAL_PSTAT_COMPLETED,
-    VCAL_PSTAT_IN_PROCESS
-} LibBalsaVCalPartStat;
-
 typedef enum {
        VEVENT_DATETIME_STAMP,
        VEVENT_DATETIME_START,
@@ -69,30 +48,38 @@ typedef enum {
 } LibBalsaVEventTimestamp;
 
 
-LibBalsaVCal *libbalsa_vcal_new(void);
 LibBalsaVCal *libbalsa_vcal_new_from_body(LibBalsaMessageBody * body);
 
-LibBalsaVEvent *libbalsa_vevent_new(void);
-gchar *libbalsa_vevent_reply(const LibBalsaVEvent * event,
-                            const gchar * sender,
-                            LibBalsaVCalPartStat new_stat);
+gchar *libbalsa_vevent_reply(const LibBalsaVEvent   *event,
+                                                const gchar            *sender,
+                                                        icalparameter_partstat  new_stat);
 
-gchar *libbalsa_vcal_attendee_to_str(LibBalsaAddress * person);
-gboolean libbalsa_vcal_attendee_rsvp(LibBalsaAddress * person);
-LibBalsaVCalMethod libbalsa_vcal_method(LibBalsaVCal *vcal);
+gchar *libbalsa_vcal_attendee_to_str(LibBalsaAddress *person);
+gboolean libbalsa_vcal_attendee_rsvp(LibBalsaAddress *person);
+icalproperty_method libbalsa_vcal_method(LibBalsaVCal *vcal);
 const gchar *libbalsa_vcal_method_str(LibBalsaVCal *vcal);
-const gchar *libbalsa_vcal_part_stat_to_str(LibBalsaVCalPartStat pstat);
-guint libbalsa_vcal_vevents(LibBalsaVCal *vcal);
-LibBalsaVEvent *libbalsa_vcal_vevent(LibBalsaVCal *vcal, guint nth_event);
+const gchar *libbalsa_vcal_part_stat_to_str(icalparameter_partstat pstat);
+guint libbalsa_vcal_vevent_count(LibBalsaVCal *vcal);
+LibBalsaVEvent *libbalsa_vcal_vevent(LibBalsaVCal *vcal,
+                                                                        guint         nth_event);
 
 LibBalsaAddress *libbalsa_vevent_organizer(LibBalsaVEvent *event);
 const gchar *libbalsa_vevent_summary(LibBalsaVEvent *event);
 const gchar *libbalsa_vevent_location(LibBalsaVEvent *event);
 const gchar *libbalsa_vevent_description(LibBalsaVEvent *event);
-time_t libbalsa_vevent_timestamp(LibBalsaVEvent *event, LibBalsaVEventTimestamp which);
-gboolean libbalsa_vevent_timestamp_date_only(LibBalsaVEvent *event, LibBalsaVEventTimestamp which);
+icalproperty_status libbalsa_vevent_status(LibBalsaVEvent *event);
+gchar *libbalsa_vevent_time_str(LibBalsaVEvent          *event,
+                                                               LibBalsaVEventTimestamp  which,
+                                                               const gchar             *format_str);
 guint libbalsa_vevent_attendees(LibBalsaVEvent *event);
-LibBalsaAddress *libbalsa_vevent_attendee(LibBalsaVEvent *event, guint nth_attendee);
+LibBalsaAddress *libbalsa_vevent_attendee(LibBalsaVEvent *event,
+                                                                                 guint           
nth_attendee);
+guint libbalsa_vevent_category_count(LibBalsaVEvent *event);
+gchar *libbalsa_vevent_category_str(LibBalsaVEvent *event);
+gchar *libbalsa_vevent_duration_str(LibBalsaVEvent *event);
+const gchar *libbalsa_vevent_status_str(LibBalsaVEvent *event);
+gchar *libbalsa_vevent_recurrence_str(LibBalsaVEvent *event,
+                                                                         const gchar    *format_str);
 
 G_END_DECLS
 
diff --git a/meson.build b/meson.build
index bd571a6bd..64e1ca627 100644
--- a/meson.build
+++ b/meson.build
@@ -136,6 +136,7 @@ gthread_dep = dependency('gthread-2.0')
 gnutls_dep  = dependency('gnutls', version : '>= 3.0')
 gpgme_dep   = dependency('gpgme', version : '>= 1.6.0')
 fribidi_dep = dependency('fribidi')
+libical_dep = dependency('libical', version : '>= 3.0.0')
 
 # Dependencies for balsa
 balsa_deps = [glib_dep,
@@ -145,7 +146,8 @@ balsa_deps = [glib_dep,
               gthread_dep,
               gnutls_dep,
               gpgme_dep,
-              fribidi_dep]
+              fribidi_dep,
+              libical_dep]
 
 # Dependencies for balsa_ab:
 balsa_ab_deps = [glib_dep,
diff --git a/src/balsa-mime-widget-vcalendar.c b/src/balsa-mime-widget-vcalendar.c
index d2e3ff347..c9fbf0d62 100644
--- a/src/balsa-mime-widget-vcalendar.c
+++ b/src/balsa-mime-widget-vcalendar.c
@@ -46,10 +46,11 @@ balsa_mime_widget_new_vcalendar(BalsaMessage * bm,
     BalsaMimeWidget *mw;
     GtkWidget *label;
     gchar *text;
+    gchar *markup_buf;
     guint event_no;
     LibBalsaMessage *lbm = balsa_message_get_message(bm);
     gboolean may_reply = FALSE;
-    InternetAddress *sender = NULL;;
+    InternetAddress *sender = NULL;
 
     g_return_val_if_fail(mime_body != NULL, NULL);
     g_return_val_if_fail(content_type != NULL, NULL);
@@ -63,14 +64,17 @@ balsa_mime_widget_new_vcalendar(BalsaMessage * bm,
 
     text = g_strdup_printf(_("This is an iTIP calendar “%s” message."),
                           libbalsa_vcal_method_str(vcal_obj));
-    label = gtk_label_new(text);
+    markup_buf = g_markup_printf_escaped("<b>%s</b>", text);
     g_free(text);
+    label = gtk_label_new(NULL);
+    gtk_label_set_markup(GTK_LABEL (label), markup_buf);
+    g_free(markup_buf);
     gtk_widget_set_halign(label, GTK_ALIGN_START);
     gtk_widget_set_valign(label, GTK_ALIGN_START);
     gtk_container_add(GTK_CONTAINER(mw), label);
 
     /* a reply may be created only for unread requests */
-    if ((libbalsa_vcal_method(vcal_obj) == ITIP_REQUEST) &&
+    if ((libbalsa_vcal_method(vcal_obj) == ICAL_METHOD_REQUEST) &&
        LIBBALSA_MESSAGE_IS_UNREAD(lbm)) {
         LibBalsaMessageHeaders *headers;
 
@@ -93,11 +97,15 @@ balsa_mime_widget_new_vcalendar(BalsaMessage * bm,
     }
 
     /* add events */
-    for (event_no = 0U; event_no < libbalsa_vcal_vevents(vcal_obj); event_no++) {
+    for (event_no = 0U; event_no < libbalsa_vcal_vevent_count(vcal_obj); event_no++) {
+       GtkWidget *frame;
        GtkWidget *event;
 
+       frame = gtk_frame_new(NULL);
+        gtk_container_add(GTK_CONTAINER(mw), frame);
        event = balsa_vevent_widget(libbalsa_vcal_vevent(vcal_obj, event_no), may_reply, sender);
-       gtk_container_add(GTK_CONTAINER(mw), event);
+        gtk_container_set_border_width(GTK_CONTAINER(event), 6U);
+       gtk_container_add(GTK_CONTAINER(frame), event);
     }
 
     g_object_unref(vcal_obj);
@@ -123,17 +131,13 @@ balsa_mime_widget_new_vcalendar(BalsaMessage * bm,
         }                                                               \
     } while (0)
 
-#define GRID_ATTACH_DATE(g,event,date_id,label)                                                        \
-       G_STMT_START {                                                                  \
-       time_t _date = libbalsa_vevent_timestamp(event, date_id);                                       \
-        if (_date != (time_t) -1) {                                                            \
-               gboolean _d_only = libbalsa_vevent_timestamp_date_only(event, date_id); \
-            gchar * _dstr =                                                                    \
-                libbalsa_date_to_utf8(_date,                                                                 
          \
-                       _d_only ? "%x" : balsa_app.date_string);                                        \
-            GRID_ATTACH(g, _dstr, label);                                                      \
-            g_free(_dstr);                                                                     \
-        }                                                                                      \
+#define GRID_ATTACH_DATE(g,event,date_id,label)                                                              
  \
+       G_STMT_START {                                                                          \
+       gchar *_dstr = libbalsa_vevent_time_str(event, date_id, balsa_app.date_string); \
+       if (_dstr != NULL) {                                                                                  
                                  \
+            GRID_ATTACH(g, _dstr, label);                                                              \
+            g_free(_dstr);                                                                             \
+        }                                                                                              \
     } G_STMT_END
 
 #define GRID_ATTACH_ADDRESS(g,addr,label)                               \
@@ -173,16 +177,33 @@ balsa_vevent_widget(LibBalsaVEvent * event, gboolean may_reply,
     int row = 0;
     LibBalsaIdentity *vevent_ident = NULL;
     guint attendee;
+    gchar *buffer;
        GString *all_atts = NULL;
 
     grid = GTK_GRID(gtk_grid_new());
     gtk_grid_set_row_spacing(grid, 6);
     gtk_grid_set_column_spacing(grid, 12);
     GRID_ATTACH(grid, libbalsa_vevent_summary(event), _("Summary:"));
+    if (libbalsa_vevent_status(event) != ICAL_STATUS_NONE) {
+       GRID_ATTACH(grid, libbalsa_vevent_status_str(event), _("Status:"));
+    }
     GRID_ATTACH_ADDRESS(grid, libbalsa_vevent_organizer(event), _("Organizer:"));
     GRID_ATTACH_DATE(grid, event, VEVENT_DATETIME_STAMP, _("Created:"));
     GRID_ATTACH_DATE(grid, event, VEVENT_DATETIME_START, _("Start:"));
     GRID_ATTACH_DATE(grid, event, VEVENT_DATETIME_END, _("End:"));
+
+    buffer = libbalsa_vevent_duration_str(event);
+    if (buffer != NULL) {
+       GRID_ATTACH(grid, buffer, _("Duration:"));
+       g_free(buffer);
+    }
+
+    buffer = libbalsa_vevent_recurrence_str(event, balsa_app.date_string);
+    if (buffer != NULL) {
+       GRID_ATTACH(grid, buffer, _("Recurrence:"));
+       g_free(buffer);
+    }
+
     GRID_ATTACH(grid, libbalsa_vevent_location(event), _("Location:"));
     for (attendee = 0U; attendee < libbalsa_vevent_attendees(event); attendee++) {
            LibBalsaAddress *lba = libbalsa_vevent_attendee(event, attendee);
@@ -215,6 +236,15 @@ balsa_vevent_widget(LibBalsaVEvent * event, gboolean may_reply,
                     ngettext("Attendee:", "Attendees:", libbalsa_vevent_attendees(event)));
        g_string_free(all_atts, TRUE);
     }
+
+    if (libbalsa_vevent_category_count(event) > 0U) {
+       gchar *categories;
+
+       categories = libbalsa_vevent_category_str(event);
+       GRID_ATTACH(grid, categories, ngettext("Category:", "Categories:", 
libbalsa_vevent_category_count(event)));
+       g_free(categories);
+    }
+
     GRID_ATTACH_TEXT(grid, libbalsa_vevent_description(event), _("Description:"));
 
     if (sender && vevent_ident) {
@@ -246,7 +276,7 @@ balsa_vevent_widget(LibBalsaVEvent * event, gboolean may_reply,
        g_object_set_data_full(G_OBJECT(button), "event", event,
                                (GDestroyNotify) g_object_unref);
        g_object_set_data(G_OBJECT(button), "mode",
-                         GINT_TO_POINTER(VCAL_PSTAT_ACCEPTED));
+                         GINT_TO_POINTER(ICAL_PARTSTAT_ACCEPTED));
        g_signal_connect(button, "clicked",
                         G_CALLBACK(vevent_reply), bbox);
        gtk_container_add(GTK_CONTAINER(bbox), button);
@@ -254,7 +284,7 @@ balsa_vevent_widget(LibBalsaVEvent * event, gboolean may_reply,
        button = gtk_button_new_with_label(_("Accept tentatively"));
        g_object_set_data(G_OBJECT(button), "event", event);
        g_object_set_data(G_OBJECT(button), "mode",
-                         GINT_TO_POINTER(VCAL_PSTAT_TENTATIVE));
+                         GINT_TO_POINTER(ICAL_PARTSTAT_TENTATIVE));
        g_signal_connect(button, "clicked",
                         G_CALLBACK(vevent_reply), bbox);
        gtk_container_add(GTK_CONTAINER(bbox), button);
@@ -262,7 +292,7 @@ balsa_vevent_widget(LibBalsaVEvent * event, gboolean may_reply,
        button = gtk_button_new_with_label(_("Decline"));
        g_object_set_data(G_OBJECT(button), "event", event);
        g_object_set_data(G_OBJECT(button), "mode",
-                         GINT_TO_POINTER(VCAL_PSTAT_DECLINED));
+                         GINT_TO_POINTER(ICAL_PARTSTAT_DECLINED));
        g_signal_connect(button, "clicked",
                         G_CALLBACK(vevent_reply), bbox);
        gtk_container_add(GTK_CONTAINER(bbox), button);
@@ -277,7 +307,7 @@ vevent_reply(GObject * button, GtkWidget * box)
 {
     LibBalsaVEvent *event =
        LIBBALSA_VEVENT(g_object_get_data(button, "event"));
-    LibBalsaVCalPartStat pstat =
+    icalparameter_partstat pstat =
        GPOINTER_TO_INT(g_object_get_data(button, "mode"));
     gchar *rcpt;
     LibBalsaMessage *message;
@@ -323,6 +353,7 @@ vevent_reply(GObject * button, GtkWidget * box)
        libbalsa_vevent_reply(event,
                              INTERNET_ADDRESS_MAILBOX(ia)->addr,
                              pstat);
+    if (body->buffer == NULL) return;
     body->charset = g_strdup("utf-8");
     body->content_type = g_strdup("text/calendar");
     libbalsa_message_append_part(message, body);
diff --git a/src/balsa-print-object-text.c b/src/balsa-print-object-text.c
index ca40a48a7..2a6a21392 100644
--- a/src/balsa-print-object-text.c
+++ b/src/balsa-print-object-text.c
@@ -458,6 +458,7 @@ balsa_print_object_text_vcard(GList               *list,
     result = balsa_print_object_default_full(list, context, pixbuf, textbuf, p_label_width, psetup);
     g_object_unref(pixbuf);
     g_free(textbuf);
+    g_object_unref(test_layout);
 
     return result;
 }
@@ -477,17 +478,13 @@ balsa_print_object_text_vcard(GList               *list,
        }                                                               \
     } while(0)
 
-#define ADD_VCAL_DATE(buf, labwidth, layout, event, date_id, descr)                                    \
-       G_STMT_START {                                                                  \
-       time_t _date = libbalsa_vevent_timestamp(event, date_id);                                       \
-        if (_date != (time_t) -1) {                                                            \
-               gboolean _d_only = libbalsa_vevent_timestamp_date_only(event, date_id); \
-            gchar * _dstr =                                                                    \
-                libbalsa_date_to_utf8(_date,                                                                 
          \
-                       _d_only ? "%x" : balsa_app.date_string);                                        \
-            ADD_VCAL_FIELD(buf, labwidth, layout, _dstr, descr);                               \
-            g_free(_dstr);                                                                     \
-        }                                                                                      \
+#define ADD_VCAL_DATE(buf, labwidth, layout, event, date_id, descr)                                          
  \
+       G_STMT_START {                                                                          \
+       gchar *_dstr = libbalsa_vevent_time_str(event, date_id, balsa_app.date_string); \
+        if (_dstr != NULL) {                                                                                 
  \
+            ADD_VCAL_FIELD(buf, labwidth, layout, _dstr, descr);                                       \
+            g_free(_dstr);                                                                             \
+        }                                                                                              \
     } G_STMT_END
 
 #define ADD_VCAL_ADDRESS(buf, labwidth, layout, addr, descr)            \
@@ -533,22 +530,39 @@ balsa_print_object_text_calendar(GList               *list,
     pango_font_description_free(header_font);
 
     /* add fields from the events*/
-    desc_buf = g_string_new("");
+    desc_buf = g_string_new(NULL);
+    g_string_append_printf(desc_buf, _("This is an iTIP calendar “%s” message."), 
libbalsa_vcal_method_str(vcal_obj));
     p_label_width = 0;
-    for (event_no = 0U; event_no < libbalsa_vcal_vevents(vcal_obj); event_no++) {
+    for (event_no = 0U; event_no < libbalsa_vcal_vevent_count(vcal_obj); event_no++) {
         LibBalsaVEvent *event = libbalsa_vcal_vevent(vcal_obj, event_no);
+        gchar *buffer;
         const gchar *description;
         guint attendees;
 
-        if (desc_buf->len > 0) {
-            g_string_append_c(desc_buf, '\n');
-        }
+        g_string_append_c(desc_buf, '\n');
         ADD_VCAL_FIELD(desc_buf, p_label_width, test_layout, libbalsa_vevent_summary(event), _("Summary:"));
+        if (libbalsa_vevent_status(event) != ICAL_STATUS_NONE) {
+               ADD_VCAL_FIELD(desc_buf, p_label_width, test_layout, libbalsa_vevent_status_str(event), 
_("Status:"));
+        }
         ADD_VCAL_ADDRESS(desc_buf, p_label_width, test_layout, libbalsa_vevent_organizer(event), 
_("Organizer:"));
         ADD_VCAL_DATE(desc_buf, p_label_width, test_layout, event, VEVENT_DATETIME_STAMP, _("Created:"));
         ADD_VCAL_DATE(desc_buf, p_label_width, test_layout, event, VEVENT_DATETIME_START, _("Start:"));
         ADD_VCAL_DATE(desc_buf, p_label_width, test_layout, event, VEVENT_DATETIME_END, _("End:"));
+
+        buffer = libbalsa_vevent_duration_str(event);
+        if (buffer != NULL) {
+               ADD_VCAL_FIELD(desc_buf, p_label_width, test_layout, buffer, _("Duration:"));
+               g_free(buffer);
+        }
+
+        buffer = libbalsa_vevent_recurrence_str(event, balsa_app.date_string);
+        if (buffer != NULL) {
+               ADD_VCAL_FIELD(desc_buf, p_label_width, test_layout, buffer, _("Recurrence:"));
+               g_free(buffer);
+        }
+
         ADD_VCAL_FIELD(desc_buf, p_label_width, test_layout, libbalsa_vevent_location(event), 
_("Location:"));
+
         attendees = libbalsa_vevent_attendees(event);
         if (attendees > 0U) {
             gchar *this_att;
@@ -564,6 +578,7 @@ balsa_print_object_text_calendar(GList               *list,
                 g_free(this_att);
             }
         }
+
         description = libbalsa_vevent_description(event);
         if (description != NULL) {
             gchar **desc_lines = g_strsplit(description, "\n", -1);
@@ -575,6 +590,21 @@ balsa_print_object_text_calendar(GList               *list,
             }
             g_strfreev(desc_lines);
         }
+
+        if (libbalsa_vevent_category_count(event) > 0U) {
+               gchar **cat_lines;
+            gint i;
+
+            buffer = libbalsa_vevent_category_str(event);
+               cat_lines = g_strsplit(buffer, "\n", -1);
+               g_free(buffer);
+               ADD_VCAL_FIELD(desc_buf, p_label_width, test_layout, cat_lines[0],
+                       ngettext("Category:", "Categories:", libbalsa_vevent_category_count(event)));
+            for (i = 1; cat_lines[i]; i++) {
+                g_string_append_printf(desc_buf, "\n\t%s", cat_lines[i]);
+            }
+            g_strfreev(cat_lines);
+        }
     }
     g_object_unref(vcal_obj);
 
@@ -586,6 +616,7 @@ balsa_print_object_text_calendar(GList               *list,
     list = balsa_print_object_default_full(list, context, pixbuf, textbuf, p_label_width, psetup);
     g_object_unref(pixbuf);
     g_free(textbuf);
+    g_object_unref(test_layout);
 
     return list;
 }
diff --git a/src/print-gtk.c b/src/print-gtk.c
index d2ca1754a..57e11dc41 100644
--- a/src/print-gtk.c
+++ b/src/print-gtk.c
@@ -131,29 +131,29 @@ static LibBalsaMessageBody *
 find_alt_part(LibBalsaMessageBody *parts,
                          gboolean                         print_alt_html)
 {
-       LibBalsaMessageBody *use_part;
+       LibBalsaMessageBody *part;
+       LibBalsaMessageBody *use_part = parts;          /* fallback: print 1st alternative part */
 
        /* scan the parts */
-       for (use_part = parts; use_part != NULL; use_part = use_part->next) {
+       for (part = parts; part != NULL; part = part->next) {
                gchar *mime_type;
                
-               mime_type = libbalsa_message_body_get_mime_type(use_part);
-               if ((strncmp(mime_type, "multipart/", 10U) == 0) && (use_part->parts != NULL)) {
+               mime_type = libbalsa_message_body_get_mime_type(part);
+               if ((strncmp(mime_type, "multipart/", 10U) == 0) && (part->parts != NULL)) {
                        /* consider the first child of a multipart */
                        g_free(mime_type);
-                       mime_type = libbalsa_message_body_get_mime_type(use_part->parts);
+                       mime_type = libbalsa_message_body_get_mime_type(part->parts);
                }
 
-               if (((strcmp(mime_type, "text/plain") == 0) && !print_alt_html) ||
+               if ((strcmp(mime_type, "text/calendar") == 0) ||
+                       ((strcmp(mime_type, "text/plain") == 0) && !print_alt_html) ||
                        ((strcmp(mime_type, "text/html") == 0) && print_alt_html)) {
-                       g_free(mime_type);
-                       return use_part;
+                       use_part = part;
                }
                g_free(mime_type);
        }
        
-       /* nothing found, fall back to the first part in the chain */
-       return parts;
+       return use_part;
 }
 
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]