Description: Calendar: limit which local files are inlined in components
 An event with an ATTACH property using a file:// URI could cause a local
 file to be read and uploaded to the calendar server by inlining it. Mangle
 file:// URIs of components coming from untrusted sources to xfile:// (keeping
 those in the backend's cache directory), and refuse to inline paths containing
 parts prefixed with "." or "etc" outside of the temporary and user cache
 directories.
 .
 Backported to 3.56: functional code from the two upstream commits applies
 unchanged; only the test registration in tests/libecal/test-cal-utils.c is
 reseated, as 3.56's main() does not contain the InitRecurTask test.
 .
 This adds public API: e_cal_util_sanitize_untrusted(),
 e_cal_util_sanitize_untrusted_full() and the ECalUtilUriNeedsChangeFunc type
 to libecal, and e_cal_backend_sanitize_untrusted() to libedata-cal.
Origin: backport, https://gitlab.gnome.org/GNOME/evolution-data-server/-/commit/2515ec687c3fccbd70bd250161e25d10a60951fd
 https://gitlab.gnome.org/GNOME/evolution-data-server/-/commit/15b3c521dd16d6b6f822332291b33f4b4cbb2db9
Bug: https://gitlab.gnome.org/GNOME/evolution-data-server/-/issues/672
Author: Sumit Verma <automationcanadian@gmail.com>
Forwarded: not-needed
Last-Update: 2026-11-08
---
--- a/src/calendar/backends/file/e-cal-backend-file.c
+++ b/src/calendar/backends/file/e-cal-backend-file.c
@@ -881,6 +881,7 @@
 static void
 scan_vcalendar (ECalBackendFile *cbfile)
 {
+	ECalBackend *cal_backend = E_CAL_BACKEND (cbfile);
 	ECalBackendFilePrivate *priv;
 	ICalCompIter *iter;
 	ICalComponent *icomp;
@@ -907,6 +908,7 @@
 				g_object_ref (icomp);
 				i_cal_object_set_owner (I_CAL_OBJECT (icomp), G_OBJECT (priv->vcalendar));
 
+				e_cal_backend_sanitize_untrusted (cal_backend, icomp);
 				check_dup_uid (cbfile, comp);
 
 				add_component (cbfile, comp, FALSE);
@@ -3478,6 +3480,7 @@
                                     GError **error)
 {
 	ESourceRegistry *registry;
+	ECalBackend *cal_backend;
 	ECalBackendFile *cbfile;
 	ECalBackendFilePrivate *priv;
 	ECalClientTzlookupICalCompData *lookup_data = NULL;
@@ -3490,6 +3493,7 @@
 	ECalBackendFileTzidData tzdata;
 	GError *err = NULL;
 
+	cal_backend = E_CAL_BACKEND (backend);
 	cbfile = E_CAL_BACKEND_FILE (backend);
 	priv = cbfile->priv;
 
@@ -3640,6 +3644,8 @@
 		if (!comp)
 			continue;
 
+		e_cal_backend_sanitize_untrusted (cal_backend, subcomp);
+
 		/* Set the created and last modified times on the component, if not there already */
 		current = i_cal_time_new_current_with_zone (i_cal_timezone_get_utc_timezone ());
 
--- a/src/calendar/libecal/e-cal-util.c
+++ b/src/calendar/libecal/e-cal-util.c
@@ -3175,6 +3175,7 @@
 					  GCancellable *cancellable,
 					  GError **error)
 {
+	GFile *cache_dir = NULL, *tmp_dir = NULL;
 	ICalProperty *prop;
 	const gchar *uid;
 	gboolean success = TRUE;
@@ -3196,53 +3197,94 @@
 			url_data = i_cal_attach_get_url (attach);
 			url = url_data ? i_cal_value_decode_ical_string (url_data) : NULL;
 
-			if (url && g_str_has_prefix (url, "file://")) {
+			if (url && g_ascii_strncasecmp (url, "file://", 7) == 0) {
 				GFile *file;
-				gchar *basename;
-				gchar *content;
-				gsize len;
+				gchar *path;
+				gboolean can;
 
 				file = g_file_new_for_uri (url);
-				basename = g_file_get_basename (file);
-				if (g_file_load_contents (file, cancellable, &content, &len, NULL, error)) {
-					ICalAttach *new_attach;
-					ICalParameter *param;
-					gchar *base64;
+				path = g_file_get_path (file);
+				can = path && *path;
 
-					base64 = g_base64_encode ((const guchar *) content, len);
-					new_attach = i_cal_attach_new_from_data (base64, (GFunc) g_free, NULL);
-					g_free (content);
+				if (can) {
+					if (!tmp_dir)
+						tmp_dir = g_file_new_for_path (g_get_tmp_dir ());
+					if (!cache_dir)
+						cache_dir = g_file_new_for_path (g_get_user_cache_dir ());
+
+					can = g_file_has_prefix (file, tmp_dir) ||
+						g_file_has_prefix (file, cache_dir);
+
+					if (!can) {
+						static const gchar *avoid_prefixes[] = { ".", "etc" };
+						gchar **parts;
+						guint ii, jj, n_parts;
+
+						parts = g_strsplit (path, G_DIR_SEPARATOR_S, -1);
+						n_parts = g_strv_length (parts);
+						can = TRUE;
+
+						for (ii = 0; can && ii < G_N_ELEMENTS (avoid_prefixes); ii++) {
+							for (jj = 0; jj < n_parts; jj++) {
+								if (g_str_has_prefix (parts[jj], avoid_prefixes[ii])) {
+									can = FALSE;
+									break;
+								}
+							}
+						}
 
-					ecu_remove_all_but_filename_parameter (prop);
+						g_strfreev (parts);
+					}
+				}
 
-					i_cal_property_set_attach (prop, new_attach);
-					g_object_unref (new_attach);
+				if (can) {
+					gchar *basename;
+					gchar *content;
+					gsize len;
+
+					basename = g_file_get_basename (file);
+					if (g_file_load_contents (file, cancellable, &content, &len, NULL, error)) {
+						ICalAttach *new_attach;
+						ICalParameter *param;
+						gchar *base64;
+
+						base64 = g_base64_encode ((const guchar *) content, len);
+						new_attach = i_cal_attach_new_from_data (base64, (GFunc) g_free, NULL);
+						g_free (content);
 
-					param = i_cal_parameter_new_value (I_CAL_VALUE_BINARY);
-					i_cal_property_take_parameter (prop, param);
+						ecu_remove_all_but_filename_parameter (prop);
 
-					param = i_cal_parameter_new_encoding (I_CAL_ENCODING_BASE64);
-					i_cal_property_take_parameter (prop, param);
+						i_cal_property_set_attach (prop, new_attach);
+						g_object_unref (new_attach);
 
-					/* Preserve existing FILENAME parameter */
-					if (!e_cal_util_property_has_parameter (prop, I_CAL_FILENAME_PARAMETER)) {
-						const gchar *use_filename = basename;
-
-						/* generated filename by Evolution */
-						if (uid && g_str_has_prefix (use_filename, uid) &&
-						    use_filename[strlen (uid)] == '-') {
-							use_filename += strlen (uid) + 1;
-						}
+						param = i_cal_parameter_new_value (I_CAL_VALUE_BINARY);
+						i_cal_property_take_parameter (prop, param);
 
-						param = i_cal_parameter_new_filename (use_filename);
+						param = i_cal_parameter_new_encoding (I_CAL_ENCODING_BASE64);
 						i_cal_property_take_parameter (prop, param);
+
+						/* Preserve existing FILENAME parameter */
+						if (!e_cal_util_property_has_parameter (prop, I_CAL_FILENAME_PARAMETER)) {
+							const gchar *use_filename = basename;
+
+							/* generated filename by Evolution */
+							if (uid && g_str_has_prefix (use_filename, uid) &&
+							    use_filename[strlen (uid)] == '-') {
+								use_filename += strlen (uid) + 1;
+							}
+
+							param = i_cal_parameter_new_filename (use_filename);
+							i_cal_property_take_parameter (prop, param);
+						}
+					} else {
+						success = FALSE;
 					}
-				} else {
-					success = FALSE;
+
+					g_free (basename);
 				}
 
 				g_object_unref (file);
-				g_free (basename);
+				g_free (path);
 			}
 
 			g_free (url);
@@ -3252,6 +3294,8 @@
 	}
 
 	g_clear_object (&prop);
+	g_clear_object (&cache_dir);
+	g_clear_object (&tmp_dir);
 
 	return success;
 }
@@ -4054,3 +4098,103 @@
 
 	return email;
 }
+
+/**
+ * e_cal_util_sanitize_untrusted_full:
+ * @comp: an #ICalComponent
+ *
+ * Sanitizes the @comp, which can be from an untrusted source,
+ * to not contain possible harmful properties or parameters.
+ *
+ * Note: This function changes API in the 3.64 series,
+ *    matching e_cal_util_sanitize_untrusted_full().
+ *
+ * Since: 3.62.1
+ **/
+void
+e_cal_util_sanitize_untrusted (ICalComponent *comp)
+{
+	g_return_if_fail (I_CAL_IS_COMPONENT (comp));
+
+	e_cal_util_sanitize_untrusted_full (comp, NULL, NULL);
+}
+
+/**
+ * e_cal_util_sanitize_untrusted_full:
+ * @comp: an #ICalComponent
+ * @uri_needs_change_func: (nullable) (scope call) (closure user_data): an #ECalUtilUriNeedsChangeFunc
+ * @user_data: user data passed to the @uri_needs_change_func
+ *
+ * Sanitizes the @comp, which can be from an untrusted source,
+ * to not contain possible harmful properties or parameters.
+ *
+ * The @uri_needs_change_func, if not %NULL, is called for each
+ * attachment URI, to check whether the URI is trusted. It can
+ * be also used to replace the attachment URI-s.
+ * When the @uri_needs_change_func is %NULL the function modifies
+ * each "file://" URI to "xfile://".
+ *
+ * Note: This function is renamed to e_cal_util_sanitize_untrusted() in the 3.64 series.
+ *
+ * Since: 3.62.2
+ **/
+void
+e_cal_util_sanitize_untrusted_full (ICalComponent *comp,
+				    ECalUtilUriNeedsChangeFunc uri_needs_change_func,
+				    gpointer user_data)
+{
+	ICalProperty *prop;
+
+	g_return_if_fail (I_CAL_IS_COMPONENT (comp));
+
+	for (prop = i_cal_component_get_first_property (comp, I_CAL_ATTACH_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (comp, I_CAL_ATTACH_PROPERTY)) {
+		ICalAttach *attach;
+
+		attach = i_cal_property_get_attach (prop);
+		if (attach && i_cal_attach_get_is_url (attach)) {
+			const gchar *url_data;
+			gchar *url = NULL;
+
+			url_data = i_cal_attach_get_url (attach);
+			url = url_data ? i_cal_value_decode_ical_string (url_data) : NULL;
+
+			if (url) {
+				gchar *new_url = NULL;
+				gboolean needs_change;
+
+				if (uri_needs_change_func) {
+					needs_change = uri_needs_change_func (url, &new_url, user_data);
+
+					if (needs_change && !new_url)
+						new_url = g_strconcat ("x", url, NULL);
+				} else {
+					needs_change = g_ascii_strncasecmp (url, "file://", 7) == 0;
+
+					/* changes "file://" URI-s to "xfile://", which is not inlined */
+					if (needs_change)
+						new_url = g_strconcat ("x", url, NULL);
+				}
+
+				if (needs_change) {
+					ICalAttach *new_attach;
+
+					new_attach = i_cal_attach_new_from_url (new_url);
+
+					i_cal_property_set_attach (prop, new_attach);
+
+					g_object_unref (new_attach);
+				}
+
+				g_free (new_url);
+			}
+
+			g_free (url);
+		}
+
+		g_clear_object (&attach);
+	}
+
+	g_clear_object (&prop);
+}
--- a/src/calendar/libecal/e-cal-util.h
+++ b/src/calendar/libecal/e-cal-util.h
@@ -493,6 +493,29 @@
 const gchar *	e_cal_util_get_attendee_email	(const ECalComponentAttendee *attendee);
 const gchar *	e_cal_util_get_property_email	(ICalProperty *prop);
 
+/**
+ * ECalUtilUriNeedsChangeFunc:
+ * @in_uri: a URI to check
+ * @out_uri: (nullable) (out) (transfer full): an out variable to optionally
+ *    set a replacement URI for the @in_uri
+ * @user_data: user data passed to the e_cal_util_sanitize_untrusted_full()
+ *
+ * Called for each attachment URI, to check the @in_uri whether it needs to be replaced.
+ * The "file://" URI-s which need to replace will be changed to "xfile://" by
+ * the caller, unless the @out_uri is populated, in which case the @out_uri
+ * is used instead.
+ *
+ * Returns: %TRUE, when the @in_uri needs to be changed, or %FALSE, when it should stay as is
+ *
+ * Since: 3.62.2
+ **/
+typedef gboolean (* ECalUtilUriNeedsChangeFunc)	(const gchar *in_uri,
+						 gchar **out_uri,
+						 gpointer user_data);
+void		e_cal_util_sanitize_untrusted	(ICalComponent *comp);
+void		e_cal_util_sanitize_untrusted_full(ICalComponent *comp,
+						 ECalUtilUriNeedsChangeFunc uri_needs_change_func,
+						 gpointer user_data);
 
 G_END_DECLS
 
--- a/src/calendar/libedata-cal/e-cal-backend.c
+++ b/src/calendar/libedata-cal/e-cal-backend.c
@@ -4704,3 +4704,81 @@
 
 	cal_backend_dispatch_next_operation (cal_backend);
 }
+
+static gboolean
+ecb_uri_needs_change_cb (const gchar *in_uri,
+			 gchar **out_uri,
+			 gpointer user_data)
+{
+	const gchar *cache_dir = user_data;
+	gboolean needs_change = FALSE;
+
+	if (!in_uri)
+		return needs_change;
+
+	if (g_ascii_strncasecmp (in_uri, "file://", 7) == 0) {
+		GFile *file;
+
+		file = g_file_new_for_uri (in_uri);
+
+		needs_change = !g_file_peek_path (file) || !cache_dir || !*cache_dir || !g_str_has_prefix (g_file_peek_path (file), cache_dir);
+
+		g_clear_object (&file);
+	} else if (in_uri && cache_dir && g_ascii_strncasecmp (in_uri, "xfile://", 8) == 0) {
+		GFile *file;
+
+		/* the local attachments in the backend cache could be incorrectly garbled,
+		   thus fix them on the fly */
+		file = g_file_new_for_uri (in_uri + 1);
+
+		if (g_file_peek_path (file) && g_str_has_prefix (g_file_peek_path (file), cache_dir)) {
+			*out_uri = g_strdup (in_uri + 1);
+			needs_change = TRUE;
+		}
+
+		g_clear_object (&file);
+	}
+
+	return needs_change;
+}
+
+/**
+ * e_cal_backend_sanitize_untrusted:
+ * @self: an #ECalBackend
+ * @comp: an #ICalComponent
+ *
+ * Sanitizes the @comp, which can be from an untrusted source,
+ * to not contain possible harmful properties or parameters.
+ *
+ * Unlike e_cal_util_sanitize_untrusted() this keeps attachment
+ * URI-s pointing to the @self cache directory untouched.
+ *
+ * Since: 3.62.2
+ **/
+void
+e_cal_backend_sanitize_untrusted (ECalBackend *self,
+				  ICalComponent *comp)
+{
+	gchar *cache_dir = NULL;
+
+	g_return_if_fail (E_IS_CAL_BACKEND (self));
+	g_return_if_fail (I_CAL_IS_COMPONENT (comp));
+
+	g_mutex_lock (&self->priv->property_lock);
+	if (self->priv->cache_dir) {
+		guint len = strlen (self->priv->cache_dir);
+
+		/* always end the cache_dir with the directory separator, to avoid prefix mismatches */
+		if (len > 0 && self->priv->cache_dir[len - 1] == G_DIR_SEPARATOR) {
+			cache_dir = g_strdup (self->priv->cache_dir);
+		} else if (len > 0) {
+			cache_dir = g_malloc0 (sizeof (gchar) * (len + 2));
+			g_warn_if_fail (g_snprintf (cache_dir, len + 2, "%s%c", self->priv->cache_dir, G_DIR_SEPARATOR) < len + 2);
+		}
+	}
+	g_mutex_unlock (&self->priv->property_lock);
+
+	e_cal_util_sanitize_untrusted_full (comp, ecb_uri_needs_change_cb, cache_dir);
+
+	g_free (cache_dir);
+}
--- a/src/calendar/libedata-cal/e-cal-backend.h
+++ b/src/calendar/libedata-cal/e-cal-backend.h
@@ -530,6 +530,9 @@
 						 gpointer user_data,
 						 GDestroyNotify user_data_free);
 
+void		e_cal_backend_sanitize_untrusted(ECalBackend *self,
+						 ICalComponent *comp);
+
 G_END_DECLS
 
 #endif /* E_CAL_BACKEND_H */
--- a/src/calendar/libedata-cal/e-cal-meta-backend.c
+++ b/src/calendar/libedata-cal/e-cal-meta-backend.c
@@ -1162,6 +1162,7 @@
 				  GError **error)
 {
 	ECacheOfflineFlag offline_flag = E_CACHE_IS_ONLINE;
+	ECalBackend *cal_backend = E_CAL_BACKEND (meta_backend);
 	ICalComponent *icomp = NULL;
 	GSList *new_instances = NULL;
 	gchar *extra = NULL;
@@ -1199,6 +1200,7 @@
 			ECalComponent *comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (subcomp));
 
 			if (comp) {
+				e_cal_backend_sanitize_untrusted (cal_backend, e_cal_component_get_icalcomponent (comp));
 				new_instances = g_slist_prepend (new_instances, comp);
 
 				if (!loaded_uid)
@@ -1211,6 +1213,7 @@
 		icomp = NULL;
 
 		if (comp) {
+			e_cal_backend_sanitize_untrusted (cal_backend, e_cal_component_get_icalcomponent (comp));
 			new_instances = g_slist_prepend (new_instances, comp);
 
 			if (!loaded_uid)
@@ -2590,6 +2593,8 @@
 	cal_backend = E_CAL_BACKEND (meta_backend);
 	registry = e_cal_backend_get_registry (cal_backend);
 
+	e_cal_backend_sanitize_untrusted (cal_backend, e_cal_component_get_icalcomponent (comp));
+
 	/* Just to check whether component exists in the cache */
 	is_in_cache = e_cal_cache_contains (cal_cache, e_cal_component_id_get_uid (id), NULL, E_CACHE_EXCLUDE_DELETED) ||
 		(e_cal_component_id_get_rid (id) && e_cal_cache_contains (cal_cache, e_cal_component_id_get_uid (id), e_cal_component_id_get_rid (id), E_CACHE_EXCLUDE_DELETED));
--- a/tests/libecal/test-cal-utils.c
+++ b/tests/libecal/test-cal-utils.c
@@ -355,6 +355,466 @@
 	g_clear_object (&new_comp);
 }
 
+#define test_param(_name, _value) G_STMT_START { \
+	gchar *param; \
+	param = i_cal_property_get_parameter_as_string (prop, _name); \
+	g_assert_cmpstr (param, ==, _value); \
+	g_free (param); \
+	} G_STMT_END
+
+static gboolean
+test_sanitize_untrusted_uri_needs_change_cb (const gchar *in_uri,
+					     gchar **out_uri,
+					     gpointer user_data)
+{
+	const gchar *good_path = user_data;
+	gboolean needs_change = FALSE;
+
+	if (!in_uri)
+		return needs_change;
+
+	if (g_ascii_strncasecmp (in_uri, "file://", 7) == 0) {
+		needs_change = !g_str_has_prefix (in_uri + 7, good_path);
+	} else if (g_ascii_strncasecmp (in_uri, "xfile://", 8) == 0) {
+		needs_change = g_str_has_prefix (in_uri + 8, good_path);
+		if (needs_change)
+			*out_uri = g_strdup (in_uri + 1);
+	}
+
+	return needs_change;
+}
+
+static void
+test_sanitize_untrusted (void)
+{
+	ICalComponent *icomp;
+	ICalProperty *prop;
+	guint nth_attach;
+
+	/* first when all are fine */
+	icomp = i_cal_component_new_from_string (
+		"BEGIN:VEVENT\r\n"
+		"UID:1\r\n"
+		"SUMMARY:first\r\n"
+		"ATTACH;FILENAME=logo.png;FMTTYPE=image/png:https://gnome.org/logo.png\r\n"
+		"ATTACH;VALUE=BINARY;FILENAME=text.txt;ENCODING=BASE64:aGVsbG8K\r\n"
+		"ATTACH:https://gnome.org/\r\n"
+		"END:VEVENT\r\n");
+	g_assert_nonnull (icomp);
+
+	for (nth_attach = 0, prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "1");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "first");
+		} else if (i_cal_property_isa (prop) == I_CAL_ATTACH_PROPERTY) {
+			ICalAttach *attach;
+
+			attach = i_cal_property_get_attach (prop);
+			g_assert_nonnull (attach);
+
+			nth_attach++;
+
+			switch (nth_attach) {
+			case 1:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "https://gnome.org/logo.png");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 2);
+				test_param ("filename", "logo.png");
+				test_param ("fmttype", "image/png");
+				break;
+			case 2:
+				g_assert_false (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_data (attach), ==, "aGVsbG8K");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 3);
+				test_param ("value", "BINARY");
+				test_param ("filename", "text.txt");
+				test_param ("encoding", "BASE64");
+				break;
+			case 3:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "https://gnome.org/");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			default:
+				g_assert_not_reached ();
+			}
+
+			g_clear_object (&attach);
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+	g_assert_cmpuint (nth_attach, ==, 3);
+
+	e_cal_util_sanitize_untrusted_full (icomp, NULL, NULL);
+
+	for (nth_attach = 0, prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "1");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "first");
+		} else if (i_cal_property_isa (prop) == I_CAL_ATTACH_PROPERTY) {
+			ICalAttach *attach;
+
+			attach = i_cal_property_get_attach (prop);
+			g_assert_nonnull (attach);
+
+			nth_attach++;
+
+			switch (nth_attach) {
+			case 1:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "https://gnome.org/logo.png");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 2);
+				test_param ("filename", "logo.png");
+				test_param ("fmttype", "image/png");
+				break;
+			case 2:
+				g_assert_false (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_data (attach), ==, "aGVsbG8K");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 3);
+				test_param ("value", "BINARY");
+				test_param ("filename", "text.txt");
+				test_param ("encoding", "BASE64");
+				break;
+			case 3:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "https://gnome.org/");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			default:
+				g_assert_not_reached ();
+			}
+
+			g_clear_object (&attach);
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+	g_clear_object (&icomp);
+	g_assert_cmpuint (nth_attach, ==, 3);
+
+	/* then when some are bad */
+	icomp = i_cal_component_new_from_string (
+		"BEGIN:VEVENT\r\n"
+		"UID:2\r\n"
+		"SUMMARY:second\r\n"
+		"ATTACH;FILENAME=logo.png;FMTTYPE=image/png:file:///tmp/image.png\r\n"
+		"ATTACH;VALUE=BINARY;FILENAME=text.txt;ENCODING=BASE64:aGVsbG8K\r\n"
+		"ATTACH:cid:part-id\r\n"
+		"ATTACH:FiLe://y.z\r\n"
+		"END:VEVENT\r\n");
+	g_assert_nonnull (icomp);
+
+	for (nth_attach = 0, prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "2");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "second");
+		} else if (i_cal_property_isa (prop) == I_CAL_ATTACH_PROPERTY) {
+			ICalAttach *attach;
+
+			attach = i_cal_property_get_attach (prop);
+			g_assert_nonnull (attach);
+
+			nth_attach++;
+
+			switch (nth_attach) {
+			case 1:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "file:///tmp/image.png");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 2);
+				test_param ("filename", "logo.png");
+				test_param ("fmttype", "image/png");
+				break;
+			case 2:
+				g_assert_false (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_data (attach), ==, "aGVsbG8K");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 3);
+				test_param ("value", "BINARY");
+				test_param ("filename", "text.txt");
+				test_param ("encoding", "BASE64");
+				break;
+			case 3:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "cid:part-id");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			case 4:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "FiLe://y.z");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			default:
+				g_assert_not_reached ();
+			}
+
+			g_clear_object (&attach);
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+	g_assert_cmpuint (nth_attach, ==, 4);
+
+	e_cal_util_sanitize_untrusted_full (icomp, NULL, NULL);
+
+	for (nth_attach = 0, prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "2");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "second");
+		} else if (i_cal_property_isa (prop) == I_CAL_ATTACH_PROPERTY) {
+			ICalAttach *attach;
+
+			attach = i_cal_property_get_attach (prop);
+			g_assert_nonnull (attach);
+
+			nth_attach++;
+
+			switch (nth_attach) {
+			case 1:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "xfile:///tmp/image.png");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 2);
+				test_param ("filename", "logo.png");
+				test_param ("fmttype", "image/png");
+				break;
+			case 2:
+				g_assert_false (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_data (attach), ==, "aGVsbG8K");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 3);
+				test_param ("value", "BINARY");
+				test_param ("filename", "text.txt");
+				test_param ("encoding", "BASE64");
+				break;
+			case 3:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "cid:part-id");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			case 4:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "xFiLe://y.z");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			default:
+				g_assert_not_reached ();
+			}
+
+			g_clear_object (&attach);
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+	g_clear_object (&icomp);
+	g_assert_cmpuint (nth_attach, ==, 4);
+
+	/* then with no ATTACH */
+	icomp = i_cal_component_new_from_string (
+		"BEGIN:VEVENT\r\n"
+		"UID:3\r\n"
+		"SUMMARY:third\r\n"
+		"END:VEVENT\r\n");
+	g_assert_nonnull (icomp);
+
+	for (prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "3");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "third");
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+
+	e_cal_util_sanitize_untrusted_full (icomp, NULL, NULL);
+
+	for (prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "3");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "third");
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+	g_clear_object (&icomp);
+
+	/* then flip the ATTACH URI-s */
+	icomp = i_cal_component_new_from_string (
+		"BEGIN:VEVENT\r\n"
+		"UID:4\r\n"
+		"SUMMARY:fourth\r\n"
+		"ATTACH:file:///good/path/file.txt\r\n"
+		"ATTACH:cid:part-id\r\n"
+		"ATTACH:file:///bad/path/bad.xml\r\n"
+		"END:VEVENT\r\n");
+	g_assert_nonnull (icomp);
+
+	for (nth_attach = 0, prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "4");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "fourth");
+		} else if (i_cal_property_isa (prop) == I_CAL_ATTACH_PROPERTY) {
+			ICalAttach *attach;
+
+			attach = i_cal_property_get_attach (prop);
+			g_assert_nonnull (attach);
+
+			nth_attach++;
+
+			switch (nth_attach) {
+			case 1:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "file:///good/path/file.txt");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			case 2:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "cid:part-id");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			case 3:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "file:///bad/path/bad.xml");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			default:
+				g_assert_not_reached ();
+			}
+
+			g_clear_object (&attach);
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+	g_assert_cmpuint (nth_attach, ==, 3);
+
+	e_cal_util_sanitize_untrusted_full (icomp, test_sanitize_untrusted_uri_needs_change_cb, (gpointer) "/good/path/");
+
+	for (nth_attach = 0, prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "4");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "fourth");
+		} else if (i_cal_property_isa (prop) == I_CAL_ATTACH_PROPERTY) {
+			ICalAttach *attach;
+
+			attach = i_cal_property_get_attach (prop);
+			g_assert_nonnull (attach);
+
+			nth_attach++;
+
+			switch (nth_attach) {
+			case 1:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "file:///good/path/file.txt");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			case 2:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "cid:part-id");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			case 3:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "xfile:///bad/path/bad.xml");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			default:
+				g_assert_not_reached ();
+			}
+
+			g_clear_object (&attach);
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+
+	e_cal_util_sanitize_untrusted_full (icomp, test_sanitize_untrusted_uri_needs_change_cb, (gpointer) "/bad/path/");
+
+	for (nth_attach = 0, prop = i_cal_component_get_first_property (icomp, I_CAL_ANY_PROPERTY);
+	     prop;
+	     g_object_unref (prop), prop = i_cal_component_get_next_property (icomp, I_CAL_ANY_PROPERTY)) {
+		if (i_cal_property_isa (prop) == I_CAL_UID_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_uid (prop), ==, "4");
+		} else if (i_cal_property_isa (prop) == I_CAL_SUMMARY_PROPERTY) {
+			g_assert_cmpstr (i_cal_property_get_summary (prop), ==, "fourth");
+		} else if (i_cal_property_isa (prop) == I_CAL_ATTACH_PROPERTY) {
+			ICalAttach *attach;
+
+			attach = i_cal_property_get_attach (prop);
+			g_assert_nonnull (attach);
+
+			nth_attach++;
+
+			switch (nth_attach) {
+			case 1:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "xfile:///good/path/file.txt");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			case 2:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "cid:part-id");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			case 3:
+				g_assert_true (i_cal_attach_get_is_url (attach));
+				g_assert_cmpstr (i_cal_attach_get_url (attach), ==, "file:///bad/path/bad.xml");
+				g_assert_cmpint (i_cal_property_count_parameters (prop), ==, 0);
+				break;
+			default:
+				g_assert_not_reached ();
+			}
+
+			g_clear_object (&attach);
+		} else {
+			g_assert_not_reached ();
+		}
+	}
+
+	g_clear_object (&prop);
+	g_clear_object (&icomp);
+	g_assert_cmpuint (nth_attach, ==, 3);
+}
+
+#undef test_param
+
 gint
 main (gint argc,
       gchar **argv)
@@ -366,6 +826,7 @@
 		e_test_server_utils_setup, test_clamp_vtimezone, e_test_server_utils_teardown);
 	g_test_add ("/ECalUtils/Categories", ETestServerFixture, &test_closure,
 		e_test_server_utils_setup, test_categories, e_test_server_utils_teardown);
+	g_test_add_func ("/ECalUtils/SanitizeUntrusted", test_sanitize_untrusted);
 
 	return e_test_server_utils_run (argc, argv);
 }
