[packages/cyrus-imapd] - updated to 3.12.3, more features, updates for libical 4

qboosh qboosh at pld-linux.org
Sat Jul 18 13:05:54 CEST 2026


commit ef2f0733262a563b6c723e1a932bfa020bc78d73
Author: Jakub Bogusz <qboosh at pld-linux.org>
Date:   Sat Jul 18 13:05:49 2026 +0200

    - updated to 3.12.3, more features, updates for libical 4

 cyrus-imapd-libical.patch | 1219 +++++++++++++++++++++++++++++++++++++++++++++
 cyrus-imapd.spec          |  217 ++++++--
 2 files changed, 1390 insertions(+), 46 deletions(-)
---
diff --git a/cyrus-imapd.spec b/cyrus-imapd.spec
index 62cb97c..27b29b4 100644
--- a/cyrus-imapd.spec
+++ b/cyrus-imapd.spec
@@ -1,29 +1,38 @@
-# TODO: zephyr notification?
+# TODO:
+# - zephyr notification?
+# - zeroskip support? (BR: zeroskip-devel, x86_64 only)
 
 #
 # Conditional build:
 %bcond_without	doc		# documentation regeneration
+%bcond_without	clamav		# ClamAV support
 %bcond_without	http		# HTTP support
 %bcond_without	ldap		# LDAP support
+%bcond_without	pgsql		# PostgreSQL backend support
+%bcond_without	perl		# Perl support
+%bcond_without	srs		# Sender Rewriting Scheme support
+%bcond_without	wslay		# WebSockets support
 %bcond_without	lmdb		# LMDB backend support
 %bcond_without	mysql		# MySQL backend support
-%bcond_without	perl		# Perl support
-%bcond_without	pgsql		# PostgreSQL backend support
 %bcond_with	sphinx		# Sphinx search engine support (broken)
 %bcond_without	xapian		# Xapian search engine support
+%bcond_without	cld2		# CLD2 language detection in Xapian indexer
 #
-#
+%if %{without xapian}
+# CLD2 used only in Xapian code, without Xapian it's irrelevant
+%undefine	with_cld2
+%endif
 Summary:	High-performance mail store with IMAP and POP3
 Summary(pl.UTF-8):	Wysoko wydajny serwer IMAP i POP3
 Summary(pt_BR.UTF-8):	Um servidor de mail de alto desempenho que suporta IMAP e POP3
 Name:		cyrus-imapd
-Version:	3.12.1
-Release:	2
+Version:	3.12.3
+Release:	1
 License:	BSD-like
 Group:		Networking/Daemons/POP3
 #Source0Download: https://github.com/cyrusimap/cyrus-imapd/releases
 Source0:	https://github.com/cyrusimap/cyrus-imapd/releases/download/%{name}-%{version}/%{name}-%{version}.tar.gz
-# Source0-md5:	1936a0e871dc9f8e269b4192c9cf0764
+# Source0-md5:	d2fc077b5cd0cd4254b7a204ee4d095e
 Source1:	cyrus-README
 Source2:	cyrus-procmailrc
 Source3:	cyrus-deliver-wrapper.c
@@ -37,40 +46,54 @@ Source11:	%{name}.init
 Source12:	cyrus.conf
 Source13:	cyrus-sync.init
 Patch0:		%{name}-et.patch
-
+Patch1:		%{name}-libical.patch
 Patch2:		%{name}-clamav-0.101.patch
 URL:		http://www.cyrusimap.org/
 BuildRequires:	autoconf >= 2.63
 BuildRequires:	automake
+%{?with_clamav:BuildRequires:	clamav-devel >= 0.73}
+%{?with_cld2:BuildRequires:	cld2-devel}
+BuildRequires:	cyrus-guesstz-devel
 BuildRequires:	cyrus-sasl-devel >= 2.1.7
 BuildRequires:	db-devel >= 4.1.25
 BuildRequires:	flex
-BuildRequires:	jansson-devel >= 2.3
+BuildRequires:	heimdal-devel
+BuildRequires:	jansson-devel >= 2.10
 %{?with_http:BuildRequires:	libbrotli-devel}
 BuildRequires:	libcap-devel
-BuildRequires:	libchardet-devel
+BuildRequires:	libchardet-devel >= 1.0.5
 BuildRequires:	libcom_err-devel >= 1.21
-%{?with_http:BuildRequires:	libical-devel >= 2.0}
-BuildRequires:	libicu-devel
+%{?with_http:BuildRequires:	libical-devel >= 4}
+BuildRequires:	libicu-devel >= 55
+%{?with_srs:BuildRequires:	libsrs2-devel}
+%{?with_xapian:BuildRequires:	libstdc++-devel >= 6:4.7}
 BuildRequires:	libtool >= 2:2.2.6
+BuildRequires:	libuuid-devel
 %{?with_http:BuildRequires:	libxml2-devel >= 1:2.7.3}
 %{?with_lmdb:BuildRequires:	lmdb-devel}
 %{?with_mysql:BuildRequires:	mysql-devel}
 BuildRequires:	net-snmp-devel
-%{?with_http:BuildRequires:	nghttp2-devel >= 1.5}
+%{?with_http:BuildRequires:	nghttp2-devel >= 1.34.0}
 %{?with_ldap:BuildRequires:	openldap-devel}
 BuildRequires:	openssl-devel >= 0.9.7d
 BuildRequires:	patchutils
+BuildRequires:	pcre2-8-devel
+BuildRequires:	pcre2-posix-devel
 %{?with_doc:BuildRequires:	perl-Pod-POM-View-Restructured}
 %{?with_perl:BuildRequires:	perl-devel >= 1:5.8.0}
+BuildRequires:	pkgconfig
 %{?with_pgsql:BuildRequires:	postgresql-devel}
+%{?with_doc:BuildRequires:	python3-git}
+%{?with_doc:BuildRequires:	python3-sphinx_rtd_theme}
 %{?with_perl:BuildRequires:	rpm-perlprov}
 BuildRequires:	rpmbuild(macros) >= 1.527
 %{?with_http:BuildRequires:	shapelib-devel >= 1.4.1}
 %{?with_http:BuildRequires:	sqlite3-devel >= 3}
-%{?with_doc:BuildRequires:	sphinx-pdg-3}
-%{?with_xapian:BuildRequires:	xapian-core-devel}
+%{?with_doc:BuildRequires:	sphinx-pdg >= 2}
+%{?with_wslay:BuildRequires:	wslay-devel >= 1.1.1}
+%{?with_xapian:BuildRequires:	xapian-core-devel >= 1.4}
 BuildRequires:	zlib-devel
+BuildRequires:	zstd-devel >= 1.4.0
 Requires(post,preun):	/sbin/chkconfig
 Requires(postun):	/usr/sbin/userdel
 Requires(pre):	/bin/id
@@ -152,7 +175,8 @@ Summary:	Shared cyrus-imapd libraries
 Summary(pl.UTF-8):	Współdzielone biblioteki cyrus-imapd
 Group:		Libraries
 Requires:	cyrus-sasl-libs >= 2.1.7
-Requires:	jansson >= 2.3
+Requires:	jansson >= 2.10
+Requires:	libcom_err >= 1.21
 
 %description libs
 Shared cyrus-imapd libraries.
@@ -166,7 +190,16 @@ Summary(pl.UTF-8):	Pliki nagłówkowe do programowania z użyciem bibliotek cyru
 Group:		Development/Libraries
 Requires:	%{name}-libs = %{version}-%{release}
 Requires:	cyrus-sasl-devel >= 2.1.7
-Requires:	jansson-devel >= 2.3
+Requires:	heimdal-devel
+Requires:	libcom_err-devel >= 1.21
+%{?with_http:Requires:	libical-devel >= 4}
+%{?with_http:Requires:	libxml2-devel >= 1:2.7.3}
+%{?with_mysql:Requires:	mysql-devel}
+Requires:	openssl-devel
+Requires:	pcre2-8-devel
+Requires:	pcre2-posix-devel
+%{?with_pgsql:Requires:	postgresql-devel}
+%{?with_http:Requires:	sqlite3-devel >= 3}
 
 %description devel
 This package provides the necessary header files files to allow you to
@@ -203,7 +236,7 @@ Perlowy interfejs do biblioteki cyrus-imapd.
 %prep
 %setup -q
 %patch -P0 -p1
-
+%patch -P1 -p1
 %patch -P2 -p1
 
 cp -p %{SOURCE1} %{SOURCE2} %{SOURCE4} %{SOURCE5} .
@@ -227,10 +260,15 @@ find docsrc/ -name '*.pyc' -print0 | xargs --null %{__rm}
 	--enable-murder \
 	--enable-nntp \
 	--enable-replication \
+	--disable-silent-rules \
 	%{__enable_disable sphinx} \
+	%{?with_srs:--enable-srs} \
 	--enable-static \
 	%{__enable_disable xapian} \
+	%{!?with_clamav:--without-clamav} \
+	%{!?with_cld2:--without-cld2} \
 	--with-com_err=/usr \
+	%{__with_without doc sphinx-build} \
 	%{__with_without ldap} \
 	--with-libcap \
 	--without-libwrap \
@@ -238,7 +276,8 @@ find docsrc/ -name '*.pyc' -print0 | xargs --null %{__rm}
 	%{__with_without mysql} \
 	%{__with_without perl perl %{__perl}} \
 	%{__with_without pgsql} \
-	%{__with_without doc sphinx-build}
+	%{__with_without wslay} \
+	--with-zoneinfo-dir=%{_datadir}/cyrus-timezones/zoneinfo
 
 %{__make} -j1 \
 	INSTALLDIRS=vendor \
@@ -251,10 +290,9 @@ find docsrc/ -name '*.pyc' -print0 | xargs --null %{__rm}
 rm -rf $RPM_BUILD_ROOT
 install -d \
 	$RPM_BUILD_ROOT{%{_sbindir},%{pkglibexecdir},%{_mandir}} \
-	$RPM_BUILD_ROOT%{_sysconfdir}/{logrotate.d,sysconfig/rc-inetd} \
 	$RPM_BUILD_ROOT/var/spool/imap/stage. \
 	$RPM_BUILD_ROOT/var/lib/imap/{user,quota,proc,log,msg,deliverdb/db,sieve,db,socket} \
-	$RPM_BUILD_ROOT/etc/{security,pam.d,rc.d/init.d}
+	$RPM_BUILD_ROOT/etc/{logrotate.d,security,pam.d,rc.d/init.d}
 
 touch $RPM_BUILD_ROOT/var/lib/imap/mailboxes \
 	$RPM_BUILD_ROOT/var/lib/imap/faillog \
@@ -269,6 +307,8 @@ touch $RPM_BUILD_ROOT/var/lib/imap/mailboxes \
 
 # ensure +x bit for rpm autodeps
 chmod +x $RPM_BUILD_ROOT%{_libdir}/*.so*
+# obsoleted by pkg-config
+%{__rm} $RPM_BUILD_ROOT%{_libdir}/libcyrus*.la
 
 install -p deliver-wrapper $RPM_BUILD_ROOT%{pkglibexecdir}/deliver-wrapper
 
@@ -340,10 +380,13 @@ fi
 %defattr(644,root,root,755)
 %doc cyrus-README cyrus-procmailrc cyrus-user-procmailrc.template
 %doc cyrus-imapd-procmail+cyrus.mc COPYING tools
-%config(noreplace) %verify(not md5 mtime size) %{_sysconfdir}/*.conf
+%config(noreplace) %verify(not md5 mtime size) %{_sysconfdir}/cyrus.conf
+%config(noreplace) %verify(not md5 mtime size) %{_sysconfdir}/imapd.conf
 %attr(640,root,root) %config(noreplace) %verify(not md5 mtime size) /etc/logrotate.d/cyrus-imapd
-%attr(440,cyrus,root) %config(noreplace) %verify(not md5 mtime size) /etc/pam.d/*
-%attr(640,root,root) %config(noreplace) %verify(not md5 mtime size) /etc/security/blacklist.*
+%attr(440,cyrus,root) %config(noreplace) %verify(not md5 mtime size) /etc/pam.d/imap
+%attr(440,cyrus,root) %config(noreplace) %verify(not md5 mtime size) /etc/pam.d/pop
+%attr(640,root,root) %config(noreplace) %verify(not md5 mtime size) /etc/security/blacklist.imap
+%attr(640,root,root) %config(noreplace) %verify(not md5 mtime size) /etc/security/blacklist.pop3
 %attr(754,root,root) /etc/rc.d/init.d/cyrus-imapd
 %attr(754,root,root) /etc/rc.d/init.d/cyrus-sync
 %attr(640,cyrus,mail) %ghost /var/lib/imap/faillog
@@ -365,7 +408,9 @@ fi
 #%attr(755,root,root) %{pkglibexecdir}/backupd
 %attr(755,root,root) %{pkglibexecdir}/calalarmd
 %attr(755,root,root) %{pkglibexecdir}/fud
-%{?with_http:%attr(755,root,root) %{pkglibexecdir}/httpd}
+%if %{with http}
+%attr(755,root,root) %{pkglibexecdir}/httpd
+%endif
 %attr(755,root,root) %{pkglibexecdir}/idled
 %attr(755,root,root) %{pkglibexecdir}/imapd
 %attr(755,root,root) %{pkglibexecdir}/lmtpd
@@ -388,7 +433,9 @@ fi
 %attr(755,root,root) %{_sbindir}/ctl_cyrusdb
 %attr(755,root,root) %{_sbindir}/ctl_deliver
 %attr(755,root,root) %{_sbindir}/ctl_mboxlist
-%{?with_http:%attr(755,root,root) %{_sbindir}/ctl_zoneinfo}
+%if %{with http}
+%attr(755,root,root) %{_sbindir}/ctl_zoneinfo
+%endif
 %attr(755,root,root) %{_sbindir}/cvt_cyrusdb
 %attr(755,root,root) %{_sbindir}/cvt_xlist_specialuse
 %attr(755,root,root) %{_sbindir}/cyr_buildinfo
@@ -405,7 +452,9 @@ fi
 %attr(755,root,root) %{_sbindir}/cyr_userseen
 %attr(755,root,root) %{_sbindir}/cyr_virusscan
 %attr(755,root,root) %{_sbindir}/cyr_withlock_run
-%{?with_http:%attr(755,root,root) %{_sbindir}/dav_reconstruct}
+%if %{with http}
+%attr(755,root,root) %{_sbindir}/dav_reconstruct
+%endif
 %attr(755,root,root) %{_sbindir}/deliver
 %attr(755,root,root) %{_sbindir}/fetchnews
 %attr(755,root,root) %{_sbindir}/ipurge
@@ -439,34 +488,107 @@ fi
 %attr(750,cyrus,mail) /var/lib/imap/socket
 %attr(750,cyrus,mail) %config(noreplace) %verify(not md5 mtime size) /var/lib/imap/mailboxes
 
-%{_mandir}/man*/*
+%{_mandir}/man1/cyradm.1p*
+%if %{with http}
+%{_mandir}/man1/dav_reconstruct.1*
+%endif
+%{_mandir}/man1/httptest.1*
+%{_mandir}/man1/imtest.1*
+%{_mandir}/man1/installsieve.1*
+%{_mandir}/man1/lmtptest.1*
+%{_mandir}/man1/mupdatetest.1*
+%{_mandir}/man1/nntptest.1*
+%{_mandir}/man1/pop3test.1*
+%{_mandir}/man1/sieveshell.1*
+%{_mandir}/man1/sivtest.1*
+%{_mandir}/man1/smtptest.1*
+%{_mandir}/man1/synctest.1*
+%{_mandir}/man5/cyrus.conf.5*
+%{_mandir}/man5/imapd.conf.5*
+%{_mandir}/man8/arbitron.8*
+%{_mandir}/man8/calalarmd.8*
+%{_mandir}/man8/chk_cyrus.8*
+%{_mandir}/man8/ctl_conversationsdb.8*
+%{_mandir}/man8/ctl_cyrusdb.8*
+%{_mandir}/man8/ctl_deliver.8*
+%{_mandir}/man8/ctl_mboxlist.8*
+%if %{with http}
+%{_mandir}/man8/ctl_zoneinfo.8*
+%endif
+%{_mandir}/man8/cvt_cyrusdb.8*
+%{_mandir}/man8/cvt_xlist_specialuse.8*
+%{_mandir}/man8/cyr_buildinfo.8*
+%{_mandir}/man8/cyr_dbtool.8*
+%{_mandir}/man8/cyr_deny.8*
+%{_mandir}/man8/cyr_df.8*
+%{_mandir}/man8/cyr_expire.8*
+%{_mandir}/man8/cyr_info.8*
+%{_mandir}/man8/cyr_ls.8*
+%{_mandir}/man8/cyr_synclog.8*
+%{_mandir}/man8/cyr_userseen.8*
+%{_mandir}/man8/cyr_virusscan.8*
+%{_mandir}/man8/cyr_withlock_run.8*
+%{_mandir}/man8/cyradm.8*
+%{_mandir}/man8/cyrdeliver.8*
+%{_mandir}/man8/cyrdump.8*
+%{_mandir}/man8/cyrmaster.8*
+%{_mandir}/man8/cyrquota.8*
+%{_mandir}/man8/cyrreconstruct.8*
+%{_mandir}/man8/fetchnews.8*
+%{_mandir}/man8/fud.8*
+%{_mandir}/man8/httpd.8*
+%{_mandir}/man8/imapd.8*
+%{_mandir}/man8/ipurge.8*
+%{_mandir}/man8/lmtpd.8*
+%{_mandir}/man8/lmtpproxyd.8*
+%{_mandir}/man8/mbexamine.8*
+%{_mandir}/man8/mbpath.8*
+%{_mandir}/man8/mbtool.8*
+%{_mandir}/man8/mupdate.8*
+%{_mandir}/man8/nntpd.8*
+%{_mandir}/man8/notifyd.8*
+%{_mandir}/man8/pop3d.8*
+%{_mandir}/man8/pop3proxyd.8*
+%{_mandir}/man8/promstatsd.8*
+%{_mandir}/man8/proxyd.8*
+%{_mandir}/man8/ptdump.8*
+%{_mandir}/man8/ptexpire.8*
+%{_mandir}/man8/ptloader.8*
+%{_mandir}/man8/relocate_by_id.8*
+%{_mandir}/man8/sievec.8*
+%{_mandir}/man8/sieved.8*
+%{_mandir}/man8/smmapd.8*
+%{_mandir}/man8/squatter.8*
+%{_mandir}/man8/sync_client.8*
+%{_mandir}/man8/sync_reset.8*
+%{_mandir}/man8/sync_server.8*
+%{_mandir}/man8/timsieved.8*
+%{_mandir}/man8/tls_prune.8*
+%{_mandir}/man8/unexpunge.8*
 
 %files libs
 %defattr(644,root,root,755)
-%attr(755,root,root) %{_libdir}/libcyrus.so.0
-%attr(755,root,root) %ghost %{_libdir}/libcyrus.so.*.*
-%attr(755,root,root) %{_libdir}/libcyrus_min.so.0
-%attr(755,root,root) %ghost %{_libdir}/libcyrus_min.so.*.*
-%attr(755,root,root) %{_libdir}/libcyrus_imap.so.0
-%attr(755,root,root) %ghost %{_libdir}/libcyrus_imap.so.*.*
-%attr(755,root,root) %{_libdir}/libcyrus_sieve.so.0
-%attr(755,root,root) %ghost %{_libdir}/libcyrus_sieve.so.*.*
+%{_libdir}/libcyrus.so.0
+%ghost %{_libdir}/libcyrus.so.*.*
+%{_libdir}/libcyrus_min.so.0
+%ghost %{_libdir}/libcyrus_min.so.*.*
+%{_libdir}/libcyrus_imap.so.0
+%ghost %{_libdir}/libcyrus_imap.so.*.*
+%{_libdir}/libcyrus_sieve.so.0
+%ghost %{_libdir}/libcyrus_sieve.so.*.*
 
 %files devel
 %defattr(644,root,root,755)
 %{_includedir}/cyrus
-%attr(755,root,root) %{_libdir}/libcyrus.so
-%attr(755,root,root) %{_libdir}/libcyrus_min.so
-%attr(755,root,root) %{_libdir}/libcyrus_imap.so
-%attr(755,root,root) %{_libdir}/libcyrus_sieve.so
-%{_libdir}/libcyrus.la
-%{_libdir}/libcyrus_min.la
-%{_libdir}/libcyrus_imap.la
-%{_libdir}/libcyrus_sieve.la
+%{_libdir}/libcyrus.so
+%{_libdir}/libcyrus_imap.so
+%{_libdir}/libcyrus_min.so
+%{_libdir}/libcyrus_sieve.so
 %{_pkgconfigdir}/libcyrus.pc
 %{_pkgconfigdir}/libcyrus_imap.pc
 %{_pkgconfigdir}/libcyrus_min.pc
 %{_pkgconfigdir}/libcyrus_sieve.pc
+%{_mandir}/man3/imclient.3*
 
 %files static
 %defattr(644,root,root,755)
@@ -481,13 +603,16 @@ fi
 %{perl_vendorarch}/Cyrus
 %dir %{perl_vendorarch}/auto/Cyrus
 %dir %{perl_vendorarch}/auto/Cyrus/IMAP
-%attr(755,root,root) %{perl_vendorarch}/auto/Cyrus/IMAP/IMAP.so
+%{perl_vendorarch}/auto/Cyrus/IMAP/IMAP.so
 %dir %{perl_vendorarch}/auto/Cyrus/SIEVE
 %dir %{perl_vendorarch}/auto/Cyrus/SIEVE/managesieve
-%attr(755,root,root) %{perl_vendorarch}/auto/Cyrus/SIEVE/managesieve/managesieve.so
+%{perl_vendorarch}/auto/Cyrus/SIEVE/managesieve/managesieve.so
 %dir %{perl_vendorlib}/Cyrus
 %dir %{perl_vendorlib}/Cyrus/Annotator
 %{perl_vendorlib}/Cyrus/Annotator/AnnotateInlinedCIDs.pm
 %{perl_vendorlib}/Cyrus/Annotator/Daemon.pm
 %{perl_vendorlib}/Cyrus/Annotator/Message.pm
+%{_mandir}/man3/Cyrus::Annotator::*.3pm*
+%{_mandir}/man3/Cyrus::IMAP*.3pm*
+%{_mandir}/man3/Cyrus::SIEVE::managesieve.3pm*
 %endif
diff --git a/cyrus-imapd-libical.patch b/cyrus-imapd-libical.patch
new file mode 100644
index 0000000..0bb884a
--- /dev/null
+++ b/cyrus-imapd-libical.patch
@@ -0,0 +1,1219 @@
+From 2734e8e48bca9918947719c794a3231ec7a0d5e7 Mon Sep 17 00:00:00 2001
+From: Robert Stepanek <rsto at fastmailteam.com>
+Date: Thu, 10 Jul 2025 16:02:51 +0200
+Subject: [PATCH] libical v4: support multivalued iCalendar parameters
+
+---
+ imap/carddav_db.c    |  3 ++-
+ imap/ical_support.c  | 22 ++++++++--------------
+ imap/jmap_contact.c  |  8 +++++---
+ imap/jmap_ical.c     |  8 ++++----
+ imap/vcard_support.c | 18 ------------------
+ imap/vcard_support.h |  2 --
+ 6 files changed, 19 insertions(+), 42 deletions(-)
+
+diff --git a/imap/carddav_db.c b/imap/carddav_db.c
+index 38ac36b2b9..44b7f724af 100644
+--- a/imap/carddav_db.c
++++ b/imap/carddav_db.c
+@@ -1482,7 +1482,8 @@ EXPORTED int carddav_writecard_x(struct carddav_db *carddavdb,
+                                                         VCARD_TYPE_PARAMETER))) {
+                 vcardenumarray *types = vcardparameter_get_type(param);
+                 vcardenumarray_element pref = { .val = VCARD_TYPE_PREF };
+-                if (vcardenumarray_find(types, &pref) >= 0) ispref = 1;
++                if (vcardenumarray_find(types, &pref) < vcardenumarray_size(types))
++                    ispref = 1;
+             }
+             strarray_appendm(&emails, propval);
+             strarray_append(&emails, ispref ? "1" : "");
+diff --git a/imap/ical_support.c b/imap/ical_support.c
+index 5202ca6793..fc2b93877c 100644
+--- a/imap/ical_support.c
++++ b/imap/ical_support.c
+@@ -1826,20 +1826,14 @@ static void apply_patch_parameter(struct path_segment_t *path_seg,
+         case ICAL_MEMBER_PARAMETER:
+             /* Multi-valued parameter */
+             if (path_seg->data) {
+-                /* Check if entire parameter value == single value */
+-                const char *single = (const char *) path_seg->data;
+-                const char *param_val = icalparameter_get_value_as_string(param);
+-
+-                if (strcmp(param_val, single)) {
+-                    /* Not an exact match, try to remove single value */
+-                    char *newval = remove_single_value(param_val, single);
+-                    if (newval) {
+-                        *num_changes += 1;
+-                        icalparameter_set_member(param, newval);
+-                        free(newval);
+-                    }
+-                    continue;
+-                }
++                icalstrarray *member = icalparameter_get_member(param);
++                size_t old_size = icalstrarray_size(member);
++                icalstrarray_remove(member, path_seg->data);
++                size_t new_size = icalstrarray_size(member);
++                if (old_size != new_size) *num_changes += 1;
++                if (!icalstrarray_size(member))
++                    icalproperty_remove_parameter_by_ref(parent, param);
++                continue;
+             }
+             break;
+ 
+diff --git a/imap/jmap_contact.c b/imap/jmap_contact.c
+index 24ec3c69e5..f041116e5e 100644
+--- a/imap/jmap_contact.c
++++ b/imap/jmap_contact.c
+@@ -6658,7 +6658,9 @@ static void jscomps_from_vcard(json_t *obj, vcardproperty *prop,
+                 /* Skip values that appear in other fields */
+                 if (ckind->flags & FIELD_EXT) {
+                     vcardstrarray *alt_sa = st->field[ckind->alt_idx];
+-                    if (alt_sa && vcardstrarray_find(alt_sa, val) >= 0)
++                    if (alt_sa
++                        && vcardstrarray_find(alt_sa, val)
++                               < vcardstrarray_size(alt_sa))
+                         continue;
+                 }
+ 
+@@ -9725,7 +9727,7 @@ static unsigned _jsname_to_vcard(struct jmap_parser *parser, json_t *jval,
+             }
+ 
+             /* Add SORT-AS parameter */
+-            vcardproperty_add_parameter(prop, vcardparameter_new_sortas(sortas));
++            vcardproperty_add_parameter(prop, vcardparameter_new_sortas_list(sortas));
+ 
+             jmap_parser_pop(parser);
+         }
+@@ -9895,7 +9897,7 @@ static vcardproperty *_jsorg_to_vcard(struct jmap_parser *parser, json_t *obj,
+ 
+         prop =  vcardproperty_new_org(units);
+         if (sortas) {
+-            vcardproperty_add_parameter(prop, vcardparameter_new_sortas(sortas));
++            vcardproperty_add_parameter(prop, vcardparameter_new_sortas_list(sortas));
+         }
+ 
+         if (!props) {
+diff --git a/imap/jmap_ical.c b/imap/jmap_ical.c
+index ae0d256a56..e46d88816d 100644
+--- a/imap/jmap_ical.c
++++ b/imap/jmap_ical.c
+@@ -2609,7 +2609,7 @@ static json_t *participant_from_ical(icalproperty *prop,
+              param;
+              param = icalproperty_get_next_parameter(prop, ICAL_DELEGATEDTO_PARAMETER)) {
+ 
+-            char *uri = normalized_uri(icalparameter_get_delegatedto(param));
++            char *uri = normalized_uri(icalparameter_get_delegatedto_nth(param, 0));
+             const char *to_id = hash_lookup(uri, id_by_uri);
+             free(uri);
+             if (to_id) json_object_set_new(delegatedTo, to_id, json_true());
+@@ -2627,7 +2627,7 @@ static json_t *participant_from_ical(icalproperty *prop,
+              param;
+              param = icalproperty_get_next_parameter(prop, ICAL_DELEGATEDFROM_PARAMETER)) {
+ 
+-            char *uri = normalized_uri(icalparameter_get_delegatedfrom(param));
++            char *uri = normalized_uri(icalparameter_get_delegatedfrom_nth(param, 0));
+             const char *from_id = hash_lookup(uri, id_by_uri);
+             free(uri);
+             if (from_id) json_object_set_new(delegatedFrom, from_id, json_true());
+@@ -2645,7 +2645,7 @@ static json_t *participant_from_ical(icalproperty *prop,
+              param;
+              param = icalproperty_get_next_parameter(prop, ICAL_MEMBER_PARAMETER)) {
+ 
+-            char *uri = normalized_uri(icalparameter_get_member(param));
++            char *uri = normalized_uri(icalparameter_get_member_nth(param, 0));
+             const char *id = hash_lookup(uri, id_by_uri);
+             char keybuf[JMAPICAL_SHA1HEXSTR_LEN];
+             if (!id) id = sha1hexstr(uri, keybuf);
+@@ -3341,7 +3341,7 @@ virtuallocations_from_ical(icalcomponent *comp)
+              param;
+              param = icalproperty_get_next_parameter(prop, ICAL_FEATURE_PARAMETER)) {
+ 
+-            switch (icalparameter_get_feature(param)) {
++            switch (icalparameter_get_feature_nth(param, 0)) {
+             case ICAL_FEATURE_AUDIO:
+                 json_object_set_new(features, "audio", json_true());
+                 break;
+diff --git a/imap/vcard_support.c b/imap/vcard_support.c
+index 0ccadc1b64..ed7062b502 100644
+--- a/imap/vcard_support.c
++++ b/imap/vcard_support.c
+@@ -799,22 +799,4 @@ EXPORTED const char *vcardproperty_get_xparam_value(vcardproperty *prop,
+     return NULL;
+ }
+ 
+-EXPORTED void vcardproperty_add_type_parameter(vcardproperty *prop,
+-                                               vcardenumarray_element *type)
+-{
+-    vcardenumarray *types;
+-    vcardparameter *param =
+-        vcardproperty_get_first_parameter(prop, VCARD_TYPE_PARAMETER);
+-
+-    if (param) {
+-        types = vcardparameter_get_type(param);
+-    }
+-    else {
+-        types = vcardenumarray_new(1);
+-        param = vcardparameter_new_type(types);
+-        vcardproperty_add_parameter(prop, param);
+-    }
+-    vcardenumarray_append(types, type);
+-}
+-
+ #endif /* HAVE_LIBVCARDVCARD */
+diff --git a/imap/vcard_support.h b/imap/vcard_support.h
+index 105fdfafba..8d168efeb3 100644
+--- a/imap/vcard_support.h
++++ b/imap/vcard_support.h
+@@ -87,8 +87,6 @@ extern size_t vcard_prop_decode_value_x(vcardproperty *prop,
+ extern const char *vcardproperty_get_xparam_value(vcardproperty *prop,
+                                                   const char *name);
+ 
+-extern void vcardproperty_add_type_parameter(vcardproperty *prop,
+-                                             vcardenumarray_element *type);
+ #endif /* HAVE_LIBICALVCARD */
+ 
+ #endif /* VCARD_SUPPORT_H */
+#From 4148707141f5bd788089adc0f72daa87ecb7ab80 Mon Sep 17 00:00:00 2001
+#From: Robert Stepanek <rsto at fastmailteam.com>
+#Date: Tue, 11 Nov 2025 22:09:46 +0100
+#Subject: [PATCH] ical_support.c: configure libical to allow empty properties
+#
+#This was the CMake build option ICAL_ALLOW_EMPTY_PROPERTIES in
+#libical v3 but now is configured at runtime.
+#---
+# imap/ical_support.c | 1 +
+# 1 file changed, 1 insertion(+)
+#
+#diff --git a/imap/ical_support.c b/imap/ical_support.c
+#index 7770fa23a8..8e814000f3 100644
+#--- a/imap/ical_support.c
+#+++ b/imap/ical_support.c
+#@@ -82,6 +82,7 @@ EXPORTED void ical_support_init(void)
+#     /* Initialize libicalvcard extended property values */
+#     vcardparser_set_xprop_value_kind(vcard_xprop_value_func, NULL);
+# #endif /* HAVE_LIBICALVCARD_XPROP_VALUE */
+#+    icalproperty_set_allow_empty_properties(true);
+# 
+#     initialized = 1;
+# }
+From 0aecffac5e72b20dbae5f0301fc8f5f70d080193 Mon Sep 17 00:00:00 2001
+From: Robert Stepanek <rsto at fastmailteam.com>
+Date: Tue, 11 Nov 2025 22:35:40 +0100
+Subject: [PATCH] ical_support.h: remove icaltimezone_set_zone_directory macro
+
+Libical v4 now defines this function.
+---
+ imap/ical_support.h | 1 -
+ 1 file changed, 1 deletion(-)
+
+diff --git a/imap/ical_support.h b/imap/ical_support.h
+index 6784d24f73..f0a7200ad2 100644
+--- a/imap/ical_support.h
++++ b/imap/ical_support.h
+@@ -257,7 +257,6 @@ extern void icalcomponent_set_jmapid(icalcomponent *comp, const char *id);
+ #endif
+ 
+ /* Functions that should be declared in libical */
+-#define icaltimezone_set_zone_directory set_zone_directory
+ 
+ #define icalcomponent_get_tzuntil_property(comp) \
+     icalcomponent_get_first_property(comp, ICAL_TZUNTIL_PROPERTY)
+From c965f01bc1c7feaa67ca31054a589853fc9ab2ff Mon Sep 17 00:00:00 2001
+From: Robert Stepanek <rsto at fastmailteam.com>
+Date: Tue, 11 Nov 2025 22:18:03 +0100
+Subject: [PATCH] Rename icaldurationtype_{from|as}_int to _seconds
+
+These got renamed in libical v4.
+---
+ imap/http_caldav.c       | 2 +-
+ imap/http_caldav_sched.c | 2 +-
+ imap/ical_support.c      | 4 ++--
+ imap/itip_support.c      | 4 ++--
+ imap/jmap_backup.c       | 2 +-
+ imap/xcal.c              | 4 ++--
+ 6 files changed, 9 insertions(+), 9 deletions(-)
+
+diff --git a/imap/http_caldav.c b/imap/http_caldav.c
+index c793f72035..f1fc999fa1 100644
+--- a/imap/http_caldav.c
++++ b/imap/http_caldav.c
+@@ -3787,7 +3787,7 @@ static int validate_dtend_duration(icalcomponent *comp, struct error_t *error)
+         if (prop) {
+             struct icaldurationtype duration = icalproperty_get_duration(prop);
+ 
+-            if (icaldurationtype_as_int(duration) < 0) {
++            if (icaldurationtype_as_seconds(duration) < 0) {
+                 /* NOTE: Per RFC 5545, Section 3.8.2.5, DURATION > 0,
+                    but DURATION == 0 occurs frequently enough in the wild
+                    for us to allow it */
+diff --git a/imap/http_caldav_sched.c b/imap/http_caldav_sched.c
+index 86f1e16cc4..6442442914 100644
+--- a/imap/http_caldav_sched.c
++++ b/imap/http_caldav_sched.c
+@@ -1473,7 +1473,7 @@ static unsigned propcmp(icalcomponent *oldical, icalcomponent *newical,
+         struct icaldurationtype olddur = icalproperty_get_duration(oldprop);
+         struct icaldurationtype newdur = icalproperty_get_duration(newprop);
+ 
+-        return (icaldurationtype_as_int(olddur) != icaldurationtype_as_int(newdur));
++        return (icaldurationtype_as_seconds(olddur) != icaldurationtype_as_seconds(newdur));
+     }
+     else if ((kind == ICAL_RDATE_PROPERTY) || (kind == ICAL_EXDATE_PROPERTY) ||
+              (kind == ICAL_ATTACH_PROPERTY)) {
+diff --git a/imap/ical_support.c b/imap/ical_support.c
+index 8e814000f3..832b9bd6a9 100644
+--- a/imap/ical_support.c
++++ b/imap/ical_support.c
+@@ -629,7 +629,7 @@ EXPORTED int icalcomponent_myforeach(icalcomponent *ical,
+         event_length = icalcomponent_get_duration(mastercomp);
+         if (icaldurationtype_is_null_duration(event_length) &&
+             icaltime_is_date(dtstart)) {
+-            event_length = icaldurationtype_from_int(60 * 60 * 24);  /* P1D */
++            event_length = icaldurationtype_from_seconds(60 * 60 * 24);  /* P1D */
+         }
+ 
+         /* add any RDATEs first, since EXDATE items can override them */
+@@ -1376,7 +1376,7 @@ icalcomponent_get_utc_timespan(icalcomponent *comp,
+                 /* DTSTART is not DATE-TIME */
+                 struct icaldurationtype dur;
+ 
+-                dur = icaldurationtype_from_int(60*60*24 - 1);  /* P1D */
++                dur = icaldurationtype_from_seconds(60*60*24 - 1);  /* P1D */
+                 icaltime_add(period.end, dur);
+             }
+         }
+diff --git a/imap/itip_support.c b/imap/itip_support.c
+index adefcec255..7d35a201da 100644
+--- a/imap/itip_support.c
++++ b/imap/itip_support.c
+@@ -970,8 +970,8 @@ static int deliver_merge_add(icalcomponent *ical,  // current iCalendar
+     };
+ 
+     if (!icaldurationtype_is_null_duration(duration) &&
+-        icaldurationtype_as_int(duration) !=
+-        icaldurationtype_as_int(icalcomponent_get_duration(master_comp))) {
++        icaldurationtype_as_seconds(duration) !=
++        icaldurationtype_as_seconds(icalcomponent_get_duration(master_comp))) {
+         /* Change in event duration */
+         rdate.period.start = dtstart;
+         rdate.period.duration = duration;
+diff --git a/imap/jmap_backup.c b/imap/jmap_backup.c
+index 279d439e8b..53feb98243 100644
+--- a/imap/jmap_backup.c
++++ b/imap/jmap_backup.c
+@@ -157,7 +157,7 @@ static void jmap_restore_parse(jmap_req_t *req,
+                 icaldurationtype_from_string(json_string_value(arg));
+ 
+             if (!icaldurationtype_is_bad_duration(dur)) {
+-                restore->cutoff = time(0) - icaldurationtype_as_int(dur);
++                restore->cutoff = time(0) - icaldurationtype_as_seconds(dur);
+             }
+         }
+ 
+diff --git a/imap/xcal.c b/imap/xcal.c
+index 6775184a65..4f8a37efbd 100644
+--- a/imap/xcal.c
++++ b/imap/xcal.c
+@@ -645,7 +645,7 @@ static icalvalue *xml_element_to_icalvalue(xmlNodePtr xtype,
+         struct icalperiodtype p;
+ 
+         p.start = p.end = icaltime_null_time();
+-        p.duration = icaldurationtype_from_int(0);
++        p.duration = icaldurationtype_from_seconds(0);
+ 
+         node = xmlFirstElementChild(xtype);
+         if (!node) {
+@@ -677,7 +677,7 @@ static icalvalue *xml_element_to_icalvalue(xmlNodePtr xtype,
+             xmlFree(content);
+             content = xmlNodeGetContent(node);
+             p.duration = icaldurationtype_from_string((const char *) content);
+-            if (icaldurationtype_as_int(p.duration) == 0) break;
++            if (icaldurationtype_as_seconds(p.duration) == 0) break;
+         }
+         else {
+             syslog(LOG_WARNING,
+From 74b139fccf1e08eaab64a5df375e1145566c12d3 Mon Sep 17 00:00:00 2001
+From: Robert Stepanek <rsto at fastmailteam.com>
+Date: Tue, 11 Nov 2025 22:29:53 +0100
+Subject: [PATCH] Replace icalenum macro invocations with actual function calls
+
+The icalenum macros get removed in libical v4.
+---
+ imap/caldav_alarm.c | 2 +-
+ imap/itip_support.c | 2 +-
+ imap/jcal.c         | 6 +++---
+ imap/jmap_ical.c    | 4 ++--
+ imap/xcal.c         | 6 +++---
+ 5 files changed, 10 insertions(+), 10 deletions(-)
+
+diff --git a/imap/caldav_alarm.c b/imap/caldav_alarm.c
+index bce4ad9d0e..622c4c5de0 100644
+--- a/imap/caldav_alarm.c
++++ b/imap/caldav_alarm.c
+@@ -807,7 +807,7 @@ static int is_supported_component(icalcomponent *ical)
+ 
+     if (!is_supported) {
+         xsyslog(LOG_DEBUG, "alarms are not supported for iCalendar component",
+-                "kind=<%s>", icalenum_component_kind_to_string(kind));
++                "kind=<%s>", icalcomponent_kind_to_string(kind));
+     }
+ 
+     return is_supported;
+diff --git a/imap/itip_support.c b/imap/itip_support.c
+index 7d35a201da..73a88c655d 100644
+--- a/imap/itip_support.c
++++ b/imap/itip_support.c
+@@ -1402,7 +1402,7 @@ HIDDEN enum sched_deliver_outcome sched_deliver_local(const char *userid,
+     default:
+         /* Unknown METHOD -- ignore it */
+         syslog(LOG_ERR, "Unknown iTIP method: %s",
+-               icalenum_method_to_string(method));
++               icalproperty_method_to_string(method));
+ 
+         sched_data->flags &= ~SCHEDFLAG_IS_REPLY;
+         goto inbox;
+diff --git a/imap/jcal.c b/imap/jcal.c
+index c3359531e5..f98709413f 100644
+--- a/imap/jcal.c
++++ b/imap/jcal.c
+@@ -644,7 +644,7 @@ static icalproperty *json_array_to_icalproperty(json_t *jprop)
+ 
+     /* Get the property type */
+     propname = ucase(icalmemory_tmp_copy(json_string_value(jtype)));
+-    kind = icalenum_string_to_property_kind(propname);
++    kind = icalproperty_string_to_kind(propname);
+     if (kind == ICAL_NO_PROPERTY) {
+         syslog(LOG_WARNING, "Unknown jCal property type: %s", propname);
+         return NULL;
+@@ -653,7 +653,7 @@ static icalproperty *json_array_to_icalproperty(json_t *jprop)
+     /* Get the value type */
+     typestr = json_string_value(jvaltype);
+     valkind = !strcmp(typestr, "unknown") ? ICAL_X_VALUE :
+-        icalenum_string_to_value_kind(ucase(icalmemory_tmp_copy(typestr)));
++        icalvalue_string_to_kind(ucase(icalmemory_tmp_copy(typestr)));
+     if (valkind == ICAL_NO_VALUE) {
+         syslog(LOG_WARNING, "Unknown jCal value type for %s property: %s",
+                propname, typestr);
+@@ -752,7 +752,7 @@ EXPORTED icalcomponent *jcal_array_as_icalcomponent(json_t *jobj)
+     }
+ 
+     type = json_string_value(jtype);
+-    kind = icalenum_string_to_component_kind(ucase(icalmemory_tmp_copy(type)));
++    kind = icalcomponent_string_to_kind(ucase(icalmemory_tmp_copy(type)));
+     if (kind == ICAL_NO_COMPONENT) {
+         syslog(LOG_WARNING, "Unknown jCal component type: %s", type);
+         return NULL;
+diff --git a/imap/jmap_ical.c b/imap/jmap_ical.c
+index 709e4de7d5..e87429f720 100644
+--- a/imap/jmap_ical.c
++++ b/imap/jmap_ical.c
+@@ -3909,7 +3909,7 @@ calendarevent_from_ical(icalcomponent *comp,
+         if (ical) {
+             icalproperty_method icalmethod = icalcomponent_get_method(ical);
+             if (icalmethod != ICAL_METHOD_NONE) {
+-                char *method = xstrdupsafe(icalenum_method_to_string(icalmethod));
++                char *method = xstrdupsafe(icalproperty_method_to_string(icalmethod));
+                 lcase(method);
+                 json_object_set_new(event, "method", json_string(method));
+                 free(method);
+@@ -7936,7 +7936,7 @@ static void calendarevent_to_ical(icalcomponent *comp,
+     jprop = json_object_get(event, "method");
+     if (json_is_string(jprop) && !is_exc) {
+         const char *val = json_string_value(jprop);
+-        icalproperty_method method = icalenum_string_to_method(val);
++        icalproperty_method method = icalproperty_string_to_method(val);
+         if (method == ICAL_METHOD_NONE) {
+             jmap_parser_invalid(parser, "method");
+         }
+diff --git a/imap/xcal.c b/imap/xcal.c
+index 4f8a37efbd..ef79196d94 100644
+--- a/imap/xcal.c
++++ b/imap/xcal.c
+@@ -852,7 +852,7 @@ static icalproperty *xml_element_to_icalproperty(xmlNodePtr xprop)
+ 
+     /* Get the property type */
+     propname = ucase(icalmemory_tmp_copy((const char *) xprop->name));
+-    kind = icalenum_string_to_property_kind(propname);
++    kind = icalproperty_string_to_kind(propname);
+     if (kind == ICAL_NO_PROPERTY) {
+         syslog(LOG_WARNING, "Unknown xCal property type: %s", propname);
+         return NULL;
+@@ -896,7 +896,7 @@ static icalproperty *xml_element_to_icalproperty(xmlNodePtr xprop)
+     }
+     typestr = ucase(icalmemory_tmp_copy((const char *) node->name));
+     valkind = !strcmp(typestr, "UNKNOWN") ? ICAL_X_VALUE :
+-        icalenum_string_to_value_kind(typestr);
++        icalvalue_string_to_kind(typestr);
+     if (valkind == ICAL_NO_VALUE) {
+         syslog(LOG_WARNING, "Unknown xCal value type for %s property: %s",
+                propname, typestr);
+@@ -974,7 +974,7 @@ static icalcomponent *xml_element_to_icalcomponent(xmlNodePtr xcomp)
+ 
+     /* Get component type */
+     kind =
+-        icalenum_string_to_component_kind(
++        icalcomponent_string_to_kind(
+             ucase(icalmemory_tmp_copy((const char *) xcomp->name)));
+     if (kind == ICAL_NO_COMPONENT) {
+         syslog(LOG_WARNING, "Unknown xCal component type: %s", xcomp->name);
+From 8f48b2f101766f0fcc7be0779a98cb16cf398c8a Mon Sep 17 00:00:00 2001
+From: Robert Stepanek <rsto at fastmailteam.com>
+Date: Tue, 11 Nov 2025 22:33:04 +0100
+Subject: [PATCH] Rename icaltime_{add|subtract} to
+ icalduration_{extend|from_times}
+
+These got renamed in libical v4.
+---
+ imap/caldav_alarm.c  |  2 +-
+ imap/http_caldav.c   | 18 +++++++++---------
+ imap/http_tzdist.c   |  6 +++---
+ imap/ical_support.c  | 20 ++++++++++----------
+ imap/itip_support.c  |  4 ++--
+ imap/jmap_calendar.c |  2 +-
+ imap/jmap_ical.c     |  8 ++++----
+ 7 files changed, 30 insertions(+), 30 deletions(-)
+
+diff --git a/imap/caldav_alarm.c b/imap/caldav_alarm.c
+index 622c4c5de0..c3c71a280f 100644
+--- a/imap/caldav_alarm.c
++++ b/imap/caldav_alarm.c
+@@ -554,7 +554,7 @@ static int process_alarm_cb(icalcomponent *comp,
+                 base = end;
+             }
+             base.is_date = 0; /* need an actual time for triggers */
+-            alarmtime = icaltime_add(base, trigger.duration);
++            alarmtime = icalduration_extend(base, trigger.duration);
+         }
+         else {
+             /* absolute */
+diff --git a/imap/http_caldav.c b/imap/http_caldav.c
+index f1fc999fa1..5bf1171cea 100644
+--- a/imap/http_caldav.c
++++ b/imap/http_caldav.c
+@@ -4778,7 +4778,7 @@ static int apply_prop_timerange(struct icalperiodtype *range,
+ 
+     if (!icaldurationtype_is_null_duration(period.duration)) {
+         /* Calculate end time from duration */
+-        period.end = icaltime_add(period.start, period.duration);
++        period.end = icalduration_extend(period.start, period.duration);
+     }
+     else if (icaltime_is_null_time(period.end)) period.end = period.start;
+ 
+@@ -5364,7 +5364,7 @@ static int expand_cb(icalcomponent *comp,
+             
+         case ICAL_DURATION_PROPERTY:
+             /* Set DURATION to be for this occurrence */
+-            icalproperty_set_duration(prop, icaltime_subtract(end, start));
++            icalproperty_set_duration(prop, icalduration_from_times(end, start));
+             break;
+             
+         case ICAL_RECURRENCEID_PROPERTY:
+@@ -5455,7 +5455,7 @@ static void limit_caldata(icalcomponent *ical, struct icalperiodtype *limit)
+             /* Calculate duration of master component */
+             dtstart = icalcomponent_get_dtstart(comp);
+             dtend = icalcomponent_get_dtend(comp);
+-            dtduration = icaltime_subtract(dtend, dtstart);
++            dtduration = icalduration_from_times(dtend, dtstart);
+             break;
+         }
+     }
+@@ -5480,7 +5480,7 @@ static void limit_caldata(icalcomponent *ical, struct icalperiodtype *limit)
+ 
+         /* Check span of original occurrence */
+         dtstart = recurid;
+-        dtend = icaltime_add(dtstart, dtduration);
++        dtend = icalduration_extend(dtstart, dtduration);
+         recurspan = icaltime_span_new(dtstart, dtend, 1);
+         if (icaltime_span_overlaps(&recurspan, &limitspan)) continue;
+ 
+@@ -7288,7 +7288,7 @@ static void add_freebusy(struct icaltimetype *recurid,
+     if (recurid) newfb->recurid = *recurid;
+ 
+     if (icaltime_is_date(*start)) {
+-        newfb->per.duration = icaltime_subtract(*end, *start);
++        newfb->per.duration = icalduration_from_times(*end, *start);
+         newfb->per.end = icaltime_null_time();
+         start->is_date = 0;  /* MUST be DATE-TIME */
+         newfb->per.start = icaltime_convert_to_zone(*start, utc_zone);
+@@ -7787,7 +7787,7 @@ HIDDEN icalcomponent *busytime_to_ical(struct freebusy_filter *fbfilter,
+ 
+         isdur = !icaldurationtype_is_null_duration(fb->per.duration);
+         end = !isdur ? fb->per.end :
+-            icaltime_add(fb->per.start, fb->per.duration);
++            icalduration_extend(fb->per.start, fb->per.duration);
+ 
+         /* Skip periods of different type or that don't overlap */
+         if ((fb->type != next_fb->type) ||
+@@ -7796,7 +7796,7 @@ HIDDEN icalcomponent *busytime_to_ical(struct freebusy_filter *fbfilter,
+         /* Coalesce into next busytime */
+         next_isdur = !icaldurationtype_is_null_duration(next_fb->per.duration);
+         next_end = !next_isdur ? next_fb->per.end :
+-            icaltime_add(next_fb->per.start, next_fb->per.duration);
++            icalduration_extend(next_fb->per.start, next_fb->per.duration);
+ 
+         if (icaltime_compare(end, next_end) >= 0) {
+             /* Current period subsumes next */
+@@ -7806,7 +7806,7 @@ HIDDEN icalcomponent *busytime_to_ical(struct freebusy_filter *fbfilter,
+         else if (isdur && next_isdur) {
+             /* Both periods are durations */
+             struct icaldurationtype overlap =
+-                icaltime_subtract(end, next_fb->per.start);
++                icalduration_from_times(end, next_fb->per.start);
+ 
+             next_fb->per.duration.days += fb->per.duration.days - overlap.days;
+         }
+@@ -8190,7 +8190,7 @@ static int meth_get_head_fb(struct transaction_t *txn, void *params)
+     }
+     else {
+         /* Set end based on period */
+-        fbfilter.end = icaltime_add(fbfilter.start, period);
++        fbfilter.end = icalduration_extend(fbfilter.start, period);
+     }
+ 
+ 
+diff --git a/imap/http_tzdist.c b/imap/http_tzdist.c
+index 9bc02e5a2d..08bc8956e6 100644
+--- a/imap/http_tzdist.c
++++ b/imap/http_tzdist.c
+@@ -1521,10 +1521,10 @@ static int action_expand(struct transaction_t *txn)
+ 
+                 /* UT and local time 1 second before onset */
+                 off.seconds = -1;
+-                ut = icaltime_add(obs->onset, off);
++                ut = icalduration_extend(obs->onset, off);
+ 
+                 off.seconds = obs->offset_from;
+-                local = icaltime_add(ut, off);
++                local = icalduration_extend(ut, off);
+ 
+                 buf_printf(body,
+                            "%s  " CTIME_FMT " UT = " CTIME_FMT " %s"
+@@ -1536,7 +1536,7 @@ static int action_expand(struct transaction_t *txn)
+                 icaltime_adjust(&ut, 0, 0, 0, 1);
+ 
+                 off.seconds = obs->offset_to;
+-                local = icaltime_add(ut, off);
++                local = icalduration_extend(ut, off);
+ 
+                 buf_printf(body,
+                            "%s  " CTIME_FMT " UT = " CTIME_FMT " %s"
+diff --git a/imap/ical_support.c b/imap/ical_support.c
+index 832b9bd6a9..176f0fe03e 100644
+--- a/imap/ical_support.c
++++ b/imap/ical_support.c
+@@ -287,7 +287,7 @@ static icaltimetype icalcomponent_get_mydtend(icalcomponent *comp)
+         } else {
+             duration = icaldurationtype_null_duration();
+         }
+-        dtend = icaltime_add(dtstart, duration);
++        dtend = icalduration_extend(dtstart, duration);
+     }
+     else dtend = dtstart;
+ 
+@@ -589,7 +589,7 @@ EXPORTED int icalcomponent_myforeach(icalcomponent *ical,
+     if (!range_span.start) range_span.start = epoch;
+     if (!range_span.end) {
+         if (!icaldurationtype_is_null_duration(range.duration)) {
+-            icaltimetype end = icaltime_add(range.start, range.duration);
++            icaltimetype end = icalduration_extend(range.start, range.duration);
+             range_span.end = icaltime_to_timet(end, NULL);
+         }
+         else range_span.end = eternity;
+@@ -644,14 +644,14 @@ EXPORTED int icalcomponent_myforeach(icalcomponent *ical,
+             icaltimetype mystart, myend;
+             if (icalperiodtype_is_null_period(rdate.period)) {
+                 mystart = rdate.time;
+-                myend = icaltime_add(mystart, event_length);
++                myend = icalduration_extend(mystart, event_length);
+             }
+             else {
+                 mystart = rdate.period.start;
+                 if (icaldurationtype_is_null_duration(rdate.period.duration))
+                     myend = rdate.period.end;
+                 else
+-                    myend = icaltime_add(mystart, rdate.period.duration);
++                    myend = icalduration_extend(mystart, rdate.period.duration);
+             }
+             if (icaltime_is_null_time(mystart))
+                 continue;
+@@ -746,7 +746,7 @@ EXPORTED int icalcomponent_myforeach(icalcomponent *ical,
+             }
+             if (icaltime_compare(xitem, ritem)) {
+                 /* not excluded - process this recurrence-id */
+-                struct icaltimetype thisend = icaltime_add(ritem, event_length);
++                struct icaltimetype thisend = icalduration_extend(ritem, event_length);
+                 icaltime_span this_span = {
+                     rtime, icaltime_to_timet(thisend, floatingtz), 0 /* is_busy */
+                 };
+@@ -1297,7 +1297,7 @@ icalcomponent_get_utc_timespan(icalcomponent *comp,
+                 struct icaldurationtype dur = icaldurationtype_null_duration();
+ 
+                 dur.days = 1;
+-                period.end = icaltime_add(period.start, dur);
++                period.end = icalduration_extend(period.start, dur);
+             }
+             else
+                 memcpy(&period.end, &period.start, sizeof(struct icaltimetype));
+@@ -1377,7 +1377,7 @@ icalcomponent_get_utc_timespan(icalcomponent *comp,
+                 struct icaldurationtype dur;
+ 
+                 dur = icaldurationtype_from_seconds(60*60*24 - 1);  /* P1D */
+-                icaltime_add(period.end, dur);
++                icalduration_extend(period.end, dur);
+             }
+         }
+         else {
+@@ -1492,9 +1492,9 @@ icalrecurrenceset_get_utc_timespan(icalcomponent *ical,
+                     struct icaldurationtype duration;
+                     icaltimezone *utc = icaltimezone_get_utc_timezone();
+ 
+-                    duration = icaltime_subtract(period.end, period.start);
++                    duration = icalduration_from_times(period.end, period.start);
+                     icaltimetype end =
+-                        icaltime_add(icaltime_convert_to_zone(recur->until, utc),
++                        icalduration_extend(icaltime_convert_to_zone(recur->until, utc),
+                                      duration);
+ 
+                     if (icaltime_compare(period.end, end) < 0)
+@@ -2187,7 +2187,7 @@ static void apply_patch_component(struct path_segment_t *path_seg,
+                     icalcomponent *override = NULL;
+                     struct icaltimetype start = path_seg->match.comp.rid;
+                     struct icaltimetype end =
+-                        icaltime_add(start, icalcomponent_get_duration(master));
++                        icalduration_extend(start, icalcomponent_get_duration(master));
+                     icalcomponent_foreach_recurrence(master, start, end,
+                                                      &create_override,
+                                                      &override);
+diff --git a/imap/itip_support.c b/imap/itip_support.c
+index 73a88c655d..724446a535 100644
+--- a/imap/itip_support.c
++++ b/imap/itip_support.c
+@@ -365,8 +365,8 @@ HIDDEN icalcomponent *master_to_recurrence(icalcomponent *master,
+         struct icaltimetype end = _get_datetime(master, endprop);
+ 
+         // calculate and re-apply the diff
+-        struct icaldurationtype diff = icaltime_subtract(end, start);
+-        struct icaltimetype newend = icaltime_add(newstart, diff);
++        struct icaldurationtype diff = icalduration_from_times(end, start);
++        struct icaltimetype newend = icalduration_extend(newstart, diff);
+ 
+         icaltimezone *endzone = (icaltimezone *)icaltime_get_timezone(end);
+         icalproperty_set_dtend(endprop,
+diff --git a/imap/jmap_calendar.c b/imap/jmap_calendar.c
+index c2b0fbaa0d..17976ae871 100644
+--- a/imap/jmap_calendar.c
++++ b/imap/jmap_calendar.c
+@@ -2771,7 +2771,7 @@ static void getcalendarevents_get_utctimes_internal(json_t *jsevent,
+     struct jmapical_datetime enddt = JMAPICAL_DATETIME_INITIALIZER;
+     icaltimetype startical = jmapical_datetime_to_icaltime(&startdt, tz);
+     struct icaldurationtype durical = jmapical_duration_to_icalduration(&dur);
+-    icaltimetype endical = icaltime_add(startical, durical);
++    icaltimetype endical = icalduration_extend(startical, durical);
+     jmapical_datetime_from_icaltime(endical, &enddt);
+ 
+     /* Convert start and end to UTC */
+diff --git a/imap/jmap_ical.c b/imap/jmap_ical.c
+index e87429f720..96e590b6f7 100644
+--- a/imap/jmap_ical.c
++++ b/imap/jmap_ical.c
+@@ -1413,7 +1413,7 @@ HIDDEN void jmapical_duration_between_unixtime(time_t t1, bit64 t1nanos,
+ 
+     icaltimetype icaltx = icaltime_from_timet_with_zone(tx, 0, utc);
+     icaltimetype icalty = icaltime_from_timet_with_zone(ty, 0, utc);
+-    struct icaldurationtype icaldur = icaltime_subtract(icalty, icaltx);
++    struct icaldurationtype icaldur = icalduration_from_times(icalty, icaltx);
+     icaldur.is_neg = is_neg;
+     jmapical_duration_from_icalduration(icaldur, dur);
+     dur->nanos = nanos;
+@@ -1592,7 +1592,7 @@ static struct icaltimetype dtend_from_ical(icalcomponent *comp,
+         } else {
+             duration = icaldurationtype_null_duration();
+         }
+-        dtend = icaltime_add(dtstart, duration);
++        dtend = icalduration_extend(dtstart, duration);
+     }
+     else dtend = dtstart;
+ 
+@@ -1952,7 +1952,7 @@ override_rdate_from_ical(icalproperty *prop)
+         /* Determine duration */
+         struct icaldurationtype icaldur;
+         if (!icaltime_is_null_time(rdate.period.end)) {
+-            icaldur = icaltime_subtract(rdate.period.end, rdate.period.start);
++            icaldur = icalduration_from_times(rdate.period.end, rdate.period.start);
+         } else {
+             icaldur = rdate.period.duration;
+         }
+@@ -4838,7 +4838,7 @@ startend_to_ical(icalcomponent *comp, struct jmap_parser *parser,
+     if (tzstart != tzend) {
+         /* Add DTEND */
+         struct icaldurationtype icaldur = jmapical_duration_to_icalduration(&dur);
+-        icaltimetype dtend = icaltime_add(dtstart, icaldur);
++        icaltimetype dtend = icalduration_extend(dtstart, icaldur);
+         dtend = icaltime_convert_to_zone(dtend, tzend);
+         icalproperty *prop = insert_icaltimeprop(comp, dtend, 1, ICAL_DTEND_PROPERTY);
+         if (prop) xjmapid_to_ical(prop, endzone_location_id);
+From b02631ad4b0cd560e20f95763d51f1ecbd60a271 Mon Sep 17 00:00:00 2001
+From: Robert Stepanek <rsto at fastmailteam.com>
+Date: Fri, 20 Feb 2026 11:41:46 +0100
+Subject: [PATCH] index.c: refactor for redefined vcardstructuredtype
+
+---
+ imap/index.c | 35 ++++++++++++++++++-----------------
+ 1 file changed, 18 insertions(+), 17 deletions(-)
+
+diff --git a/imap/index.c b/imap/index.c
+index 95b50a79f0..66dbc4f6d2 100644
+--- a/imap/index.c
++++ b/imap/index.c
+@@ -4692,8 +4692,6 @@ static int extract_vcardbuf(struct buf *raw, charset_t charset, int encoding,
+     for (prop = vcardcomponent_get_first_property(vcard, VCARD_ANY_PROPERTY);
+          prop;
+          prop = vcardcomponent_get_next_property(vcard, VCARD_ANY_PROPERTY)) {
+-        vcardstructuredtype *stp = NULL;
+-        vcardstructuredtype st = { 1, { 0 } };
+         const char *val;
+ 
+         switch (vcardproperty_isa(prop)) {
+@@ -4724,23 +4722,26 @@ static int extract_vcardbuf(struct buf *raw, charset_t charset, int encoding,
+             break;
+ 
+         case VCARD_ORG_PROPERTY: {
+-            unsigned f;
+-            size_t v;
+-
+-            st.field[0] = vcardproperty_get_org(prop);
+-            stp = &st;
+-
+-            GCC_FALLTHROUGH
++            vcardstrarray *sa = vcardproperty_get_org(prop);
++            for (size_t i = 0; i < vcardstrarray_size(sa); i++) {
++                val = vcardstrarray_element_at(sa, i);
++                if (val && val[0]) {
++                    if (buf_len(&buf)) buf_putc(&buf, ' ');
++                    buf_appendcstr(&buf, val);
++                }
++            }
++            break;
++        }
+ 
+         case VCARD_N_PROPERTY:
+-        case VCARD_ADR_PROPERTY:
+-            if (!stp) stp = vcardproperty_get_adr(prop);
+-
+-            for (f = 0; f < stp->num_fields; f++) {
+-                vcardstrarray *vals = stp->field[f];
+-
+-                for (v = 0; vals && v < vcardstrarray_size(vals); v++) {
+-                    val = vcardstrarray_element_at(vals, v);
++        case VCARD_ADR_PROPERTY: {
++            vcardstructuredtype *stt =
++                vcardvalue_get_structured(vcardproperty_get_value(prop));
++
++            for (size_t i = 0; i < vcardstructured_num_fields(stt); i++) {
++                vcardstrarray *sa = vcardstructured_field_at(stt, i);
++                for (size_t j = 0; j < vcardstrarray_size(sa); j++) {
++                    val = vcardstrarray_element_at(sa, j);
+                     if (val && val[0]) {
+                         if (buf_len(&buf)) buf_putc(&buf, ' ');
+                         buf_appendcstr(&buf, val);
+From 89595af2fc27b4ef48633bebbeb2af8fdb926fb5 Mon Sep 17 00:00:00 2001
+From: Robert Stepanek <rsto at fastmailteam.com>
+Date: Fri, 20 Feb 2026 13:17:11 +0100
+Subject: [PATCH] jmap_contact.c: refactor for redefined vcardstructuredtype
+
+---
+ imap/jmap_contact.c | 158 +++++++++++++++++++++-----------------------
+ 1 file changed, 76 insertions(+), 82 deletions(-)
+
+diff --git a/imap/jmap_contact.c b/imap/jmap_contact.c
+index ac669af9c9..5bb6f8fb71 100644
+--- a/imap/jmap_contact.c
++++ b/imap/jmap_contact.c
+@@ -6882,7 +6882,7 @@ static void jscomps_from_vcard(json_t *obj, vcardproperty *prop,
+ 
+         json_object_set_new(obj, "isOrdered", json_true());
+ 
+-        sa = jscomps->field[0];
++        sa = vcardstructured_field_at(jscomps, 0);
+         if (sa) {
+             /* add default separator, if not " " */
+             val = vcardstrarray_element_at(sa, 1);
+@@ -6891,10 +6891,10 @@ static void jscomps_from_vcard(json_t *obj, vcardproperty *prop,
+             }
+         }
+ 
+-        for (i = 1; i < jscomps->num_fields; i++) {
++        for (i = 1; i < vcardstructured_num_fields(jscomps); i++) {
+             const char *kind;
+ 
+-            sa = jscomps->field[i];
++            sa = vcardstructured_field_at(jscomps, i);
+             val = vcardstrarray_element_at(sa, 0);
+             if (*val == 's') {
+                 kind = "separator";
+@@ -6904,14 +6904,14 @@ static void jscomps_from_vcard(json_t *obj, vcardproperty *prop,
+                 int field_idx = atoi(val);
+                 int val_idx = 0;
+ 
+-                if (field_idx >= (int) st->num_fields) continue;
++                if (field_idx >= (int) vcardstructured_num_fields(st)) continue;
+ 
+                 kind = comp_kinds[field_idx].name;
+ 
+                 if (vcardstrarray_size(sa) > 1)
+                     val_idx = atoi(vcardstrarray_element_at(sa, 1));
+ 
+-                val = vcardstrarray_element_at(st->field[field_idx], val_idx);
++                val = vcardstrarray_element_at(vcardstructured_field_at(st, field_idx), val_idx);
+             }
+ 
+             if (*val) {
+@@ -6938,18 +6938,18 @@ static void jscomps_from_vcard(json_t *obj, vcardproperty *prop,
+ 
+     /* Iterate through all components and values */
+     for (const struct comp_kind *ckind = comp_kinds;
+-         ckind->name && ckind->idx < st->num_fields; ckind++) {
+-        if ((ckind->flags & FIELD_SKIP) && st->num_fields > ckind->alt_idx)
++         ckind->name && ckind->idx < vcardstructured_num_fields(st); ckind++) {
++        if ((ckind->flags & FIELD_SKIP) && vcardstructured_num_fields(st) > ckind->alt_idx)
+             continue;
+ 
+-        sa = st->field[ckind->idx];
++        sa = vcardstructured_field_at(st, ckind->idx);
+         for (i = 0; sa && i < vcardstrarray_size(sa); i++) {
+             val = vcardstrarray_element_at(sa, i);
+ 
+             if (*val) {
+                 /* Skip values that appear in other fields */
+                 if (ckind->flags & FIELD_EXT) {
+-                    vcardstrarray *alt_sa = st->field[ckind->alt_idx];
++                    vcardstrarray *alt_sa = vcardstructured_field_at(st, ckind->alt_idx);
+                     if (alt_sa
+                         && vcardstrarray_find(alt_sa, val)
+                                < vcardstrarray_size(alt_sa))
+#@@ -7048,30 +7048,30 @@ static json_t *_jsonline_from_vcard(vcardproperty *prop,
+#         const char *val = vcardproperty_get_value_as_string(prop);
+#         if (!val) goto done;
+# 
+#-        vcardstructuredtype *stt = vcardstructured_from_string(val);
+#+        vcardstructuredtype *stt = vcardstructured_new_from_string(val);
+#         if (!stt) goto done;
+# 
+#         // At least one of user and uri must be set.
+#         const char *user = NULL, *uri = NULL;
+#-        if (stt->num_fields == 2) {
+#-            if (vcardstrarray_size(stt->field[0]) == 1) {
+#-                user = vcardstrarray_element_at(stt->field[0], 0);
+#+        if (vcardstructured_num_fields(stt) == 2) {
+#+            if (vcardstrarray_size(vcardstructured_field_at(stt, 0)) == 1) {
+#+                user = vcardstrarray_element_at(vcardstructured_field_at(stt, 0), 0);
+#                 if (!strlen(user)) user = NULL;
+#             }
+#-            if (vcardstrarray_size(stt->field[1]) == 1) {
+#-                uri = vcardstrarray_element_at(stt->field[1], 0);
+#+            if (vcardstrarray_size(vcardstructured_field_at(stt, 1)) == 1) {
+#+                uri = vcardstrarray_element_at(vcardstructured_field_at(stt, 1), 0);
+#                 if (!strlen(uri)) uri = NULL;
+#             }
+#         }
+# 
+#         if (!user && !uri) {
+#-            vcardstructured_free(stt);
+#+            vcardstructured_unref(stt);
+#             goto done;
+#         }
+# 
+#         // Convert OnlineService object.
+#         jprop = json_pack("{s:s* s:s*}", "user", user, "uri", uri);
+#-        vcardstructured_free(stt);
+#+        vcardstructured_unref(stt);
+# 
+#         *param_flags =
+#             ALLOW_TYPE_PARAM | ALLOW_PREF_PARAM | ALLOW_LABEL_PARAM |
+#@@ -7458,7 +7458,7 @@ static void jsprop_from_vcard(vcardproperty *prop, json_t *obj,
+#             vcardstrarray *sorts = vcardparameter_get_sortas(param);
+#             json_t *sortas = json_object();
+# 
+#-            if (vcardstrarray_size(sorts) <= n->num_fields) {
+#+            if (vcardstrarray_size(sorts) <= vcardstructured_num_fields(n)) {
+#                 const struct comp_kind *ckind;
+#                 for (ckind = n_comp_kinds; ckind->name; ckind++) {
+#                     if (ckind->idx < vcardstrarray_size(sorts)) {
+#@@ -7472,8 +7472,8 @@ static void jsprop_from_vcard(vcardproperty *prop, json_t *obj,
+#                         // component must also be set in the N property.
+#                         bool have_ncomp = false;
+# 
+#-                        if (ckind->idx < n->num_fields) {
+#-                            vcardstrarray *ncomp = n->field[ckind->idx];
+#+                        if (ckind->idx < vcardstructured_num_fields(n)) {
+#+                            vcardstrarray *ncomp = vcardstructured_field_at(n, ckind->idx);
+#                             const char *nval = vcardstrarray_element_at(ncomp, 0);
+#                             if (nval && *nval) {
+#                                 have_ncomp = true;
+@@ -9968,8 +9968,8 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+         return 0;
+     }
+ 
+-    vcardstructuredtype vals = { args->num_comps, { 0 } };
+-    vcardstructuredtype ph = { args->num_comps, { 0 } };
++    vcardstructuredtype *vals = vcardstructured_new(args->num_comps);
++    vcardstructuredtype *ph = vcardstructured_new(args->num_comps);
+     size_t i, size = json_array_size(comps);
+     vcardstructuredtype *jscomps = NULL;
+     vcardproperty *prop = NULL;
+@@ -9999,9 +9999,7 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+     }
+ 
+     if (isordered) {
+-        jscomps = xzmalloc(sizeof(vcardstructuredtype));
+-
+-        jscomps->num_fields = 1; // for separator, regardless if specified
++        jscomps = vcardstructured_new(1); // for separator, regardless if specified
+ 
+         jprop = json_object_get(obj, "defaultSeparator");
+         if (json_is_string(jprop)) {
+@@ -10012,7 +10010,7 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+                 entry = vcardstrarray_new(2);
+                 vcardstrarray_append(entry, "s");
+                 vcardstrarray_append(entry, defsep);
+-                jscomps->field[0] = entry;
++                vcardstructured_set_field_at(jscomps, 0, entry);
+             }
+         }
+         else if (jprop) {
+@@ -10074,7 +10072,8 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+                 entry = vcardstrarray_new(2);
+                 vcardstrarray_append(entry, "s");
+                 vcardstrarray_append(entry, buf_cstring(&buf));
+-                jscomps->field[jscomps->num_fields++] = entry;
++                vcardstructured_set_field_at(jscomps,
++                        vcardstructured_num_fields(jscomps), entry);
+             }
+             else {
+                 jmap_parser_invalid(parser, "kind");
+@@ -10084,7 +10083,7 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+         else {
+             const struct comp_kind *ckind =
+                 _field_name_to_kind(kind, args->comp_kinds);
+-            vcardstrarray **field;
++            vcardstrarray *field;
+ 
+             if (!ckind) {
+                 jmap_parser_pop(parser);
+@@ -10094,15 +10093,21 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+ 
+             /* Add phonetic to proper field */
+             if (phonetic) {
+-                field = &ph.field[ckind->idx];
+-                if (!*field) *field = vcardstrarray_new(1);
+-                vcardstrarray_append(*field, phonetic);
++                field = vcardstructured_field_at(ph, ckind->idx);
++                if (!field) {
++                    field = vcardstrarray_new(1);
++                    vcardstructured_set_field_at(ph, ckind->idx, field);
++                }
++                vcardstrarray_append(field, phonetic);
+             }
+ 
+             /* Add value to proper field */
+-            field = &vals.field[ckind->idx];
+-            if (!*field) *field = vcardstrarray_new(1);
+-            vcardstrarray_append(*field, val);
++            field = vcardstructured_field_at(vals, ckind->idx);
++            if (!field) {
++                field = vcardstrarray_new(1);
++                vcardstructured_set_field_at(vals, ckind->idx, field);
++            }
++            vcardstrarray_append(field, val);
+ 
+             if (isordered) {
+                 /* Add positional entry (field idx [value idx]) to JSCOMPS */
+@@ -10110,12 +10115,13 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+                 buf_reset(&buf);
+                 buf_printf(&buf, "%d", ckind->idx);
+                 vcardstrarray_append(entry, buf_cstring(&buf));
+-                if (vcardstrarray_size(*field) > 1) {
++                if (vcardstrarray_size(field) > 1) {
+                     buf_reset(&buf);
+-                    buf_printf(&buf, "%lu", vcardstrarray_size(*field) - 1);
++                    buf_printf(&buf, "%lu", vcardstrarray_size(field) - 1);
+                     vcardstrarray_append(entry, buf_cstring(&buf));
+                 }
+-                jscomps->field[jscomps->num_fields++] = entry;
++                vcardstructured_set_field_at(jscomps,
++                        vcardstructured_num_fields(jscomps), entry);
+             }
+ 
+             if (args->derived) {
+@@ -10127,9 +10133,12 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+ 
+             /* Also add values from ext fields to backward-compat fields */
+             if (ckind->flags & FIELD_BWD) {
+-                field = &vals.field[ckind->alt_idx];
+-                if (!*field) *field = vcardstrarray_new(1);
+-                vcardstrarray_append(*field, val);
++                field = vcardstructured_field_at(vals, ckind->alt_idx);
++                if (!field) {
++                    field = vcardstrarray_new(1);
++                    vcardstructured_set_field_at(vals, ckind->alt_idx, field);
++                }
++                vcardstrarray_append(field, val);
+             }
+         }
+ 
+@@ -10141,7 +10150,7 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+         goto fail;
+     }
+ 
+-    prop = args->vanew_prop(&vals,
++    prop = args->vanew_prop(vals,
+                             jscomps ? vcardparameter_new_jscomps(jscomps) : NULL,
+                             NULL);
+ 
+@@ -10153,7 +10162,7 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+ 
+         vcardparameter_set_value_from_string(param, ph_system);
+         vcardcomponent_add_property(card,
+-                                    args->vanew_prop(&ph,
++                                    args->vanew_prop(ph,
+                                                      vcardparameter_new_altid(args->id),
+                                                      param,
+                                                      NULL));
+@@ -10166,29 +10175,17 @@ static vcardproperty *_jscomps_to_vcard(struct jmap_parser *parser, json_t *obj,
+ 
+     buf_free(&buf);
+ 
++    vcardstructured_unref(vals);
++    vcardstructured_unref(ph);
++    vcardstructured_unref(jscomps);
++
+     return prop;
+ 
+   fail:
+-    for (unsigned i = 0; i < vals.num_fields; i++) {
+-        vcardstrarray *sa = vals.field[i];
+-
+-        if (sa) vcardstrarray_free(sa);
+-    }
+-    if (ph_system) {
+-        for (unsigned i = 0; i < ph.num_fields; i++) {
+-            vcardstrarray *sa = ph.field[i];
++    vcardstructured_unref(vals);
++    vcardstructured_unref(ph);
++    vcardstructured_unref(jscomps);
+ 
+-            if (sa) vcardstrarray_free(sa);
+-        }
+-    }
+-    if (jscomps) {
+-        for (unsigned i = 0; i < jscomps->num_fields; i++) {
+-            vcardstrarray *sa = jscomps->field[i];
+-
+-            if (sa) vcardstrarray_free(sa);
+-        }
+-        free(jscomps);
+-    }
+     if (args->derived) {
+         buf_reset(args->derived);
+     }
+#@@ -10780,20 +10777,20 @@ static vcardproperty *_jsonline_to_vcard(struct jmap_parser *parser, json_t *obj
+#       json_object_del(jvcardparams, "x-user");
+# 
+#     } else {
+#-      // Use X-CYRUS-ONLINESERVICE in all other cases.
+#+        // Use X-CYRUS-ONLINESERVICE in all other cases.
+#+        vcardstructuredtype *st = vcardstructured_new(2);
+#+        vcardstrarray *field = vcardstrarray_new(1);
+#+        if (user) vcardstrarray_add(field, user);
+#+        vcardstructured_set_field_at(st, 0, field);
+#+
+#+        field = vcardstrarray_new(1);
+#+        if (uri) vcardstrarray_add(field, uri);
+#+        vcardstructured_set_field_at(st, 1, field);
+#+
+#         prop = vcardproperty_new(VCARD_X_PROPERTY);
+#         vcardproperty_set_x_name(prop, "X-CYRUS-ONLINESERVICE");
+#-        vcardproperty_set_value(prop, vcardvalue_new(VCARD_STRUCTURED_VALUE));
+#-
+#-        vcardstructuredtype *st =
+#-            vcardvalue_get_structured(vcardproperty_get_value(prop));
+#-        st->field[0] = vcardstrarray_new(1);
+#-        if (user)
+#-            vcardstrarray_add(st->field[0], user);
+#-        st->field[1] = vcardstrarray_new(1);
+#-        if (uri)
+#-            vcardstrarray_add(st->field[1], uri);
+#-        st->num_fields = 2;
+#+        vcardproperty_set_value(prop, vcardvalue_new_structured(st));
+#+        vcardstructured_unref(st);
+# 
+#         if (service) {
+#             vcardparameter *param = vcardparameter_new_x(service);
+#@@ -11555,8 +11552,7 @@ static unsigned _vcardprops_to_card(struct jmap_parser *parser, json_t *jprops,
+# 
+#         case JSON_ARRAY:
+#         // a structured value MUST be the only one & MUST NOT have too many comps
+#-        if (json_array_size(jprop) > 4 ||
+#-            json_array_size(jval) > VCARD_MAX_STRUCTURED_FIELDS) goto error;
+#+        if (json_array_size(jprop) > 4 || json_array_size(jval) > 20) goto error;
+# 
+#         default:
+#             break;
+#@@ -11583,7 +11579,7 @@ static unsigned _vcardprops_to_card(struct jmap_parser *parser, json_t *jprops,
+#         r = 1;
+# 
+#         if (json_is_array(jval)) {
+#-            vcardstructuredtype *st = vcardstructured_new();
+#+            vcardstructuredtype *st = vcardstructured_new(0);
+#             json_t *jcomp;
+#             size_t j;
+# 
+#@@ -11600,18 +11596,16 @@ static unsigned _vcardprops_to_card(struct jmap_parser *parser, json_t *jprops,
+#                 }
+# 
+#                 if (!vals) {
+#-                    vcardstructured_free(st);
+#+                    vcardstructured_unref(st);
+#                     goto error;
+#                 }
+# 
+#-                st->field[st->num_fields++] = vals;
+#+                vcardstructured_set_field_at(st,
+#+                        vcardstructured_num_fields(st), vals);
+#             }
+# 
+#             vcardproperty_set_value(prop, vcardvalue_new_structured(st));
+#-
+#-            // don't free strarrays which have been stolen by the vcardvalue
+#-            st->num_fields = 0;
+#-            vcardstructured_free(st);
+#+            vcardstructured_unref(st);
+#         }
+#         else if (val_type) {
+#             vals = jprop_values_to_strarray(jprop, 3, val_type, &buf);
+From 6f4b580768d9a5b52b5fd101349a8afb5c269378 Mon Sep 17 00:00:00 2001
+From: Robert Stepanek <rsto at fastmailteam.com>
+Date: Thu, 16 Apr 2026 08:32:12 +0200
+Subject: [PATCH] jcal.c: do not use libical-internal macro definitions
+
+Libical v4.0 removed a couple of formerly publicy defined macros.
+This patch replaces invocations of these macros with their actual
+implementation.
+---
+ imap/jcal.c | 5 ++++-
+ 1 file changed, 4 insertions(+), 1 deletion(-)
+
+diff --git a/imap/jcal.c b/imap/jcal.c
+index 8be890b370..e066c68493 100644
+--- a/imap/jcal.c
++++ b/imap/jcal.c
+@@ -89,7 +89,10 @@ static json_t *icalreqstattype_as_json_array(struct icalreqstattype stat)
+     json_t *jstat;
+     char code[22];
+ 
+-    icalerror_check_arg_rz((stat.code != ICAL_UNKNOWN_STATUS),"Status");
++    if (stat.code != ICAL_UNKNOWN_STATUS)  {
++        icalerror_set_errno(ICAL_BADARG_ERROR);
++        return NULL;
++    }
+ 
+     if (!stat.desc) stat.desc = icalenum_reqstat_desc(stat.code);
+ 
================================================================

---- gitweb:

http://git.pld-linux.org/gitweb.cgi/packages/cyrus-imapd.git/commitdiff/ef2f0733262a563b6c723e1a932bfa020bc78d73



More information about the pld-cvs-commit mailing list