Submitted By:            Pierre Labastie <pierre dot labastie at neuf dot fr>
Date:                    2026-06-11
Initial Package Version: 2.6.5
Upstream Status:         Committed
Origin:                  Upstream in "libical-4" branch
Description:             Fix building with libical-4

From fbe0d311130ce5e9259d816c54c9339d04869411 Mon Sep 17 00:00:00 2001
From: Peter Bloomfield <PeterBloomfield@bellsouth.net>
Date: Sat, 30 May 2026 15:49:11 -0400
Subject: [PATCH] rfc2445: Build with libical-4

Use the macro ICAL_CHECK_VERSION(4, 0, 0) to conditionally compile code
for version 4 of libical.

See https://discourse.gnome.org/t/libical-4-is-out-balsa-needs-updates-for-it/35205
---
 libbalsa/rfc2445.c | 254 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 254 insertions(+)

diff --git a/libbalsa/rfc2445.c b/libbalsa/rfc2445.c
index 98d5108e6..701c63b96 100644
--- a/libbalsa/rfc2445.c
+++ b/libbalsa/rfc2445.c
@@ -57,7 +57,11 @@ struct _LibBalsaVEvent {
     icaltimetype end;
     icaltimetype recurrence_id;
     struct icaldurationtype duration;
+#if ICAL_CHECK_VERSION(4, 0, 0)
+    struct icalrecurrencetype *rrule;
+#else /* ICAL_CHECK_VERSION(4, 0, 0) */
     struct icalrecurrencetype rrule;
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
     icalproperty_status status;
     gchar *uid;
     int sequence;
@@ -143,7 +147,11 @@ libbalsa_vevent_init(LibBalsaVEvent *self)
     self->end = icaltime_null_time();
     self->stamp = icaltime_null_time();
     self->duration = icaldurationtype_null_duration();
+#if ICAL_CHECK_VERSION(4, 0, 0)
+    self->rrule = icalrecurrencetype_new();
+#else /* ICAL_CHECK_VERSION(4, 0, 0) */
     self->rrule.freq = ICAL_NO_RECURRENCE;
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
     self->status = ICAL_STATUS_NONE;
 }
 
@@ -160,6 +168,9 @@ libbalsa_vevent_finalize(GObject *self)
 	if (vevent->attendee) {
 		g_list_free_full(vevent->attendee, g_object_unref);
 	}
+#if ICAL_CHECK_VERSION(4, 0, 0)
+	icalrecurrencetype_unref(vevent->rrule);
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
 	g_free(vevent->uid);
 	g_free(vevent->summary);
 	g_free(vevent->location);
@@ -501,12 +512,14 @@ libbalsa_vcal_vevent(LibBalsaVCal *vcal, guint nth_event)
  * 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... */
 
+#if ! ICAL_CHECK_VERSION(4, 0, 0)
 static inline gboolean
 ical_ar_empty(const short *array)
 {
 	return array[0] == ICAL_RECURRENCE_ARRAY_MAX;
 }
 
+#endif /* ! ICAL_CHECK_VERSION(4, 0, 0) */
 
 static inline const gchar *
 day_name(gint day)
@@ -536,10 +549,18 @@ ical_check_bydays(const struct icalrecurrencetype *rrule, guint want_days)
 {
 	guint have_days = 0U;
 	guint n;
+#if ICAL_CHECK_VERSION(4, 0, 0)
+	const icalrecurrence_by_data *by_day = &rrule->by[ICAL_BY_DAY];
+
+	for (n = 0U; n < by_day->size; n++) {
+		have_days |= (1U << by_day->data[n]);
+	}
+#else /* ICAL_CHECK_VERSION(4, 0, 0) */
 
 	for (n = 0U; (n < ICAL_BY_DAY_SIZE) && (rrule->by_day[n] != ICAL_RECURRENCE_ARRAY_MAX); n++) {
 		have_days |= (1U << rrule->by_day[n]);
 	}
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
 	return want_days == have_days;
 }
 
@@ -565,6 +586,36 @@ static GString *
 vevent_recurrence_weekly(const struct icalrecurrencetype *rrule)
 {
 	GString *result = g_string_new(NULL);
+#if ICAL_CHECK_VERSION(4, 0, 0)
+	const icalrecurrence_by_data *by_day = &rrule->by[ICAL_BY_DAY];
+
+	if (by_day->size > 0) {
+		gint n;
+
+		if (rrule->interval == 1) {
+			g_string_append(result, _("every "));
+		} else {
+			/* #1: interval */
+			g_string_append_printf(result, _("every %d weeks on "), rrule->interval);
+		}
+		g_string_append(result, day_name(by_day->data[0] - 1));
+		for (n = 1; n < by_day->size; n++) {
+			if (n < by_day->size - 1) {
+				g_string_append_printf(result, ", %s", day_name(by_day->data[n] - 1));
+			} else {
+				/* #1: the day of week (defined in the day_of_week context) */
+				g_string_append_printf(result, _(" and %s"), day_name(by_day->data[n] - 1));
+			}
+		}
+	} else {
+		if (rrule->interval == 1) {
+			g_string_append(result, _("every week"));
+		} else {
+			/* #1: interval */
+			g_string_append_printf(result, _("every %d weeks"), rrule->interval);
+		}
+	}
+#else /* ICAL_CHECK_VERSION(4, 0, 0) */
 
 	if (!ical_ar_empty(rrule->by_day)) {
 		gint n;
@@ -592,6 +643,7 @@ vevent_recurrence_weekly(const struct icalrecurrencetype *rrule)
 			g_string_append_printf(result, _("every %d weeks"), rrule->interval);
 		}
 	}
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
 	return result;
 }
 
@@ -633,6 +685,94 @@ static GString *
 vevent_recurrence_monthly(const struct icalrecurrencetype *rrule, const icaltimetype *start)
 {
 	GString *result = g_string_new(NULL);
+#if ICAL_CHECK_VERSION(4, 0, 0)
+	const icalrecurrence_by_data *by_day = &rrule->by[ICAL_BY_DAY];
+	const icalrecurrence_by_data *by_month_day = &rrule->by[ICAL_BY_MONTH_DAY];
+
+	if (by_day->size > 0) {
+		/* 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 {
+				/* #1: interval */
+				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 < by_day->size; n++) {
+				int day_pos;
+
+				day_pos = icalrecurrencetype_day_position(by_day->data[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(by_day->data[n]);
+					every_mask |= 1U << day_of_week;
+					/* #1: the day of week (defined in the day_of_week context) */
+					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 < by_day->size; n++) {
+				int day_of_week;
+
+				day_of_week = icalrecurrencetype_day_day_of_week(by_day->data[n]);
+				if ((every_mask & (1U << day_of_week)) == 0U) {
+					int day_pos;
+					GString *buffer = g_string_new(NULL);
+
+					day_pos = icalrecurrencetype_day_position(by_day->data[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 {
+					/* #1: recurrence expression */
+					g_string_append_printf(result, _(" and %s"), (const gchar *) p->data);
+				}
+			}
+			g_list_free_full(days, g_free);
+		}
+	} else if (by_month_day->size > 0) {
+		/* we have a "BYMONTHDAY" rule */
+		guint n;
+
+		for (n = 0; n < by_month_day->size; n++) {
+			if (by_month_day->data[n] < -1) {
+				return g_string_assign(result, _("rule too complex"));
+			} else {
+				if (n > 0) {
+					if (by_month_day->size > n + 1) {
+						g_string_append(result, ", ");
+					} else {
+						g_string_append(result, _(" and "));
+					}
+				}
+				day_ordinal_append(result, by_month_day->data[n], _("day"), NULL);
+			}
+		}
+	} else {
+		day_ordinal_append(result, start->day, _("day"), NULL);
+	}
+#else /* ICAL_CHECK_VERSION(4, 0, 0) */
 
 	if (!ical_ar_empty(rrule->by_day)) {
 		/* we have a "BYDAY" rule */
@@ -717,6 +857,7 @@ vevent_recurrence_monthly(const struct icalrecurrencetype *rrule, const icaltime
 	} else {
 		day_ordinal_append(result, start->day, _("day"), NULL);
 	}
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
 
 	if (rrule->interval == 1) {
 		g_string_append(result, _(" of every month"));
@@ -747,6 +888,67 @@ vevent_recurrence_yearly(const struct icalrecurrencetype *rrule, const icaltimet
 		NC_("name_of_month", "December")
 	};
 	GString *result = g_string_new(NULL);
+#if ICAL_CHECK_VERSION(4, 0, 0)
+	const icalrecurrence_by_data *by_day = &rrule->by[ICAL_BY_DAY];
+	const icalrecurrence_by_data *by_month = &rrule->by[ICAL_BY_MONTH];
+	const icalrecurrence_by_data *by_month_day = &rrule->by[ICAL_BY_MONTH_DAY];
+
+	/* rules which are too complex for Lightning, so ignore them here, too... */
+	if ((by_month->size > 1) || (by_month_day->size > 1) ||
+		(by_month_day->data[0] < -1)) {
+		return g_string_assign(result, _("rule too complex"));
+	}
+
+	if (by_day->size == 0) {
+        /* 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 (by_month->size == 0) {
+			month = mon_name[start->month - 1];
+		} else {
+			month = mon_name[by_month->data[0] - 1];
+		}
+		if (by_month_day->size == 0) {
+			day_ordinal_append(day, start->day, NULL, NULL);
+		} else {
+			day_ordinal_append(day, by_month_day->data[0], _("day"), NULL);
+		}
+		if (rrule->interval == 1) {
+			/* #1: name of month (defined in the name_of_month context) */
+			/* #2: day of week (defined in the day_of_week context) */
+			g_string_append_printf(result, _("every %s %s"), month, day->str);
+		} else {
+			/* #1: interval */
+			/* #2: name of month (defined in the name_of_month context) */
+			/* #3: day of week (defined in the day_of_week context) */
+			g_string_append_printf(result, _("every %d years on %s %s"), rrule->interval, month, day->str);
+		}
+		g_string_free(day, TRUE);
+	} else if (by_month->size > 0 && by_day->size > 0) {
+		/* RRULE:FREQ=YEARLY;BYMONTH=x;BYDAY=y1,y2,... */
+		const gchar *month = mon_name[by_month->data[0] - 1];
+
+		if (ical_check_bydays(rrule, 0x7cU)) {
+			/* every day of the month */
+			if (rrule->interval == 1) {
+				/* #1: name of month (defined in the name_of_month context) */
+				g_string_append_printf(result, _("every day of %s"), month);
+			} else {
+				/* #1: interval */
+				/* #2: name of month (defined in the name_of_month context) */
+				g_string_append_printf(result, _("every %d years every day of %s"), rrule->interval, month);
+			}
+		} else if (by_day->size == 1) {
+			int day_pos;
+			int day_of_week;
+
+			day_pos = icalrecurrencetype_day_position(by_day->data[0]);
+			day_of_week = icalrecurrencetype_day_day_of_week(by_day->data[0]);
+#else /* ICAL_CHECK_VERSION(4, 0, 0) */
 
 	/* 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) ||
@@ -803,6 +1005,7 @@ vevent_recurrence_yearly(const struct icalrecurrencetype *rrule, const icaltimet
 
 			day_pos = icalrecurrencetype_day_position(rrule->by_day[0]);
 			day_of_week = icalrecurrencetype_day_day_of_week(rrule->by_day[0]);
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
 			if (day_pos == 0) {
 				if (rrule->interval == 1) {
 					/* #1: day of week (defined in the day_of_week context) */
@@ -853,6 +1056,53 @@ libbalsa_vevent_recurrence_str(LibBalsaVEvent *event, const gchar *format_str)
 
 	g_return_val_if_fail(LIBBALSA_IS_VEVENT(event), NULL);
 
+#if ICAL_CHECK_VERSION(4, 0, 0)
+	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(C_("ical_recurrence", "secondly"));
+			break;
+		case ICAL_MINUTELY_RECURRENCE:
+			buffer = g_string_new(C_("ical_recurrence", "minutely"));
+			break;
+		case ICAL_HOURLY_RECURRENCE:
+			buffer = g_string_new(C_("ical_recurrence", "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(buffer, ngettext(", %d occurrence", ", %d occurrences", event->rrule->count));
+		} else if (icaltime_is_null_time(event->rrule->until) == 0) {
+			gchar *timestr;
+
+			timestr = icaltime_str(event->rrule->until, format_str);
+			/* #1: time string */
+			g_string_append_printf(buffer, _(" until %s"), timestr);
+			g_free(timestr);
+		} else {
+			/* nothing to do */
+		}
+
+		result = g_string_free(buffer, FALSE);
+	}
+#else /* ICAL_CHECK_VERSION(4, 0, 0) */
 	if ((event->rrule.freq >= ICAL_SECONDLY_RECURRENCE) && (event->rrule.freq <= ICAL_YEARLY_RECURRENCE)) {
 		GString *buffer;
 
@@ -898,6 +1148,7 @@ libbalsa_vevent_recurrence_str(LibBalsaVEvent *event, const gchar *format_str)
 
 		result = g_string_free(buffer, FALSE);
 	}
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
 	return result;
 }
 
@@ -1055,6 +1306,9 @@ vcalendar_extract(const gchar *vcal_buf)
 			/* RRULE (only one is allowed, see RFC 5546, Sect. 3.2.2) */
 			prop = icalcomponent_get_first_property(item, ICAL_RRULE_PROPERTY);
 			if (prop != NULL) {
+#if ICAL_CHECK_VERSION(4, 0, 0)
+				icalrecurrencetype_unref(event->rrule);
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
 				event->rrule = icalproperty_get_rrule(prop);
 			}
 
-- 
GitLab

From 84ce43ebf7706b4cd0d5b765ce973a627ae7b5ef Mon Sep 17 00:00:00 2001
From: Peter Bloomfield <PeterBloomfield@bellsouth.net>
Date: Sun, 31 May 2026 15:54:57 -0400
Subject: [PATCH] rfc2445: Normalize event duration

In libical-4, icalcomponent_get_duration() may return an icaldurationtype
structure with only the seconds member non-zero, which is presumably why
there is now icaldurationtype_normalize(); so use it.
---
 libbalsa/rfc2445.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/libbalsa/rfc2445.c b/libbalsa/rfc2445.c
index 701c63b96..50132757d 100644
--- a/libbalsa/rfc2445.c
+++ b/libbalsa/rfc2445.c
@@ -1293,6 +1293,9 @@ vcalendar_extract(const gchar *vcal_buf)
 			if (icaldurationtype_is_bad_duration(event->duration)) {
 				event->duration = icaldurationtype_null_duration();
 			}
+#if ICAL_CHECK_VERSION(4, 0, 0)
+                        event->duration = icaldurationtype_normalize(event->duration);
+#endif /* ICAL_CHECK_VERSION(4, 0, 0) */
 
 			/* RECURRENCE-ID */
 			event->recurrence_id = icalcomponent_get_recurrenceid(item);
-- 
GitLab

