From: Alessandro Astone <alessandro.astone@canonical.com>
Date: Thu, 13 Nov 2025 17:34:05 +0100
Subject: Support sandboxed mail clients

Export the attachments to the document portal

Bug-Ubuntu: https://launchpad.net/bugs/2072569
---
 meson.build                              |   3 +-
 src/meson.build                          |  12 +-
 src/nautilus-sendto.c                    |  47 +++--
 src/org.freedesktop.portal.Documents.xml | 304 +++++++++++++++++++++++++++++++
 src/xdp-documents.c                      | 161 ++++++++++++++++
 src/xdp-documents.h                      |  15 ++
 6 files changed, 527 insertions(+), 15 deletions(-)
 create mode 100644 src/org.freedesktop.portal.Documents.xml
 create mode 100644 src/xdp-documents.c
 create mode 100644 src/xdp-documents.h

diff --git a/meson.build b/meson.build
index 71033d6..46813eb 100644
--- a/meson.build
+++ b/meson.build
@@ -14,8 +14,9 @@ gio_req     = '>= 2.25.9'
 
 # Dependencies
 gio_dep = dependency('gio-2.0', version : gio_req)
+gio_unix_dep = dependency('gio-unix-2.0', version : gio_req)
 
-nautilus_sendto_deps = [gio_dep]
+nautilus_sendto_deps = [gio_dep, gio_unix_dep]
 
 # directories
 localedir = join_paths(get_option('prefix'), get_option('datadir'), 'locale')
diff --git a/src/meson.build b/src/meson.build
index a9638c1..96e33aa 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,7 +1,17 @@
+gnome = import('gnome')
+
+xdp_src = gnome.gdbus_codegen('xdp-documents-generated',
+    sources: 'org.freedesktop.portal.Documents.xml',
+    interface_prefix: 'org.freedesktop.portal',
+    namespace: 'Xdp'
+)
+
 executable('nautilus-sendto',
            'nautilus-sendto.c',
+           'xdp-documents.c',
+           xdp_src,
            include_directories: [config_inc],
-           dependencies: [gio_dep],
+           dependencies: nautilus_sendto_deps,
            install_dir: nst_bindir,
            install: true)
 
diff --git a/src/nautilus-sendto.c b/src/nautilus-sendto.c
index 9154e09..38e7fc7 100644
--- a/src/nautilus-sendto.c
+++ b/src/nautilus-sendto.c
@@ -29,6 +29,8 @@
 #include <glib/gstdio.h>
 #include <gio/gio.h>
 
+#include "xdp-documents.h"
+
 /* Options */
 static char **filenames = NULL;
 static gboolean run_from_build_dir = FALSE;
@@ -48,6 +50,7 @@ typedef struct {
 	guint num_dirs;
 	MailerType type;
 	char *mail_cmd;
+	char *app_id;
 } NautilusSendto;
 
 static const GOptionEntry entries[] = {
@@ -84,9 +87,11 @@ init_mailer (NautilusSendto *nst)
 	app_info = g_app_info_get_default_for_uri_scheme ("mailto");
 	if (app_info) {
 		nst->mail_cmd = g_strdup (g_app_info_get_commandline (app_info));
+		nst->app_id = get_sandboxed_app_id (app_info);
 		g_object_unref (app_info);
 	} else {
 		nst->mail_cmd = NULL;
+		nst->app_id = NULL;
 	}
 
 	if (nst->mail_cmd == NULL || *nst->mail_cmd == '\0') {
@@ -342,11 +347,16 @@ get_sylpheed_mailto (GString         *mailto,
 		g_string_append_printf (mailto," \"%s\"", (char *)l->data);
 }
 
+static char *escape_ampersands_and_commas (const char *uri);
+
 static void
 send_files (NautilusSendto *nst)
 {
 	GString *mailto;
 	char *cmd;
+	GList *uris = NULL;
+	GList *l;
+	g_autoptr (GError) error = NULL;
 
 	if (nst->num_dirs > 0) {
 		char *zip;
@@ -355,20 +365,37 @@ send_files (NautilusSendto *nst)
 		nst->file_list = g_list_append (NULL, zip);
 	}
 
+	if (nst->app_id) {
+		uris = document_portal_add_documents (nst->file_list, nst->app_id, &error);
+		if (!uris)
+			g_warning ("Could not add files to the xdg document portal: %s", error->message);
+	}
+	if (!uris)
+		uris = g_list_copy_deep (nst->file_list, (GCopyFunc)g_strdup, NULL);
+
+	for (l = uris; l; l = l->next) {
+		char *uri = l->data;
+		char *escaped = escape_ampersands_and_commas (uri);
+		if (escaped) {
+			l->data = escaped;
+			g_free (uri);
+		}
+	}
+
 	mailto = g_string_new ("");
 	switch (nst->type) {
 	case MAILER_BALSA:
-		get_balsa_mailto (nst, mailto, nst->file_list);
+		get_balsa_mailto (nst, mailto, uris);
 		break;
 	case MAILER_SYLPHEED:
-		get_sylpheed_mailto (mailto, nst->file_list);
+		get_sylpheed_mailto (mailto, uris);
 		break;
 	case MAILER_THUNDERBIRD:
-		get_thunderbird_mailto (mailto, nst->file_list);
+		get_thunderbird_mailto (mailto, uris);
 		break;
 	case MAILER_EVO:
 	default:
-		get_evo_mailto (mailto, nst->file_list);
+		get_evo_mailto (mailto, uris);
 	}
 
 #pragma GCC diagnostic push
@@ -383,6 +410,7 @@ send_files (NautilusSendto *nst)
 	/* FIXME: collect errors from this call */
 	g_spawn_command_line_async (cmd, NULL);
 	g_free (cmd);
+	g_list_free_full (uris, g_free);
 }
 
 static char *
@@ -464,7 +492,7 @@ nautilus_sendto_init (NautilusSendto *nst)
 	/* Clean up the URIs passed, and collect whether we have directories */
 	for (i = 0; filenames != NULL && filenames[i] != NULL; i++) {
 		GFile *file;
-		char *filename, *escaped, *uri;
+		char *filename, *uri;
 		GFileInfo *info;
 		const char *mimetype;
 
@@ -518,14 +546,7 @@ nautilus_sendto_init (NautilusSendto *nst)
 
 		uri = g_filename_to_uri (filename, NULL, NULL);
 		g_free (filename);
-		escaped = escape_ampersands_and_commas (uri);
-
-		if (escaped == NULL) {
-			nst->file_list = g_list_prepend (nst->file_list, uri);
-		} else {
-			nst->file_list = g_list_prepend (nst->file_list, escaped);
-			g_free (uri);
-		}
+		nst->file_list = g_list_prepend (nst->file_list, uri);
 	}
 
 	nst->file_list = g_list_reverse (nst->file_list);
diff --git a/src/org.freedesktop.portal.Documents.xml b/src/org.freedesktop.portal.Documents.xml
new file mode 100644
index 0000000..4e2b264
--- /dev/null
+++ b/src/org.freedesktop.portal.Documents.xml
@@ -0,0 +1,304 @@
+<?xml version="1.0"?>
+
+<!--
+ Copyright (C) 2015 Red Hat, Inc.
+
+ SPDX-License-Identifier: LGPL-2.1-or-later
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General
+ Public License along with this library; if not, write to the
+ Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA 02110-1301, USA.
+
+ Author: Alexander Larsson <alexl@redhat.com>
+-->
+
+<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+  <!--
+      org.freedesktop.portal.Documents:
+      @short_description: Document portal
+
+      The document portal allows to make files from the outside world
+      available to sandboxed applications in a controlled way.
+
+      Exported files will be made accessible to the application via
+      a fuse filesystem that gets mounted at ``/run/user/$UID/doc/``. The
+      filesystem gets mounted both outside and inside the sandbox, but
+      the view inside the sandbox is restricted to just those files
+      that the application is allowed to access.
+
+      Individual files will appear at ``/run/user/$UID/doc/$DOC_ID/filename``,
+      where $DOC_ID is the ID of the file in the document store. It is
+      returned by the org.freedesktop.portal.Documents.Add() and
+      org.freedesktop.portal.Documents.AddNamed() calls.
+
+      The permissions that the application has for a document store entry
+      (see org.freedesktop.portal.Documents.GrantPermissions()) are reflected
+      in the POSIX mode bits in the fuse filesystem.
+
+      The D-Bus interface for the document portal is available under the
+      bus name org.freedesktop.portal.Documents and the object path
+      /org/freedesktop/portal/documents.
+
+      This documentation describes version 5 of this interface.
+  -->
+  <interface name="org.freedesktop.portal.Documents">
+    <property name="version" type="u" access="read"/>
+
+    <!--
+        GetMountPoint:
+        @path: the path at which the fuse filesystem is mounted
+
+        Returns the path at which the document store fuse filesystem
+        is mounted. This will typically be ``/run/user/$UID/doc/``.
+    -->
+    <method name="GetMountPoint">
+      <arg type="ay" name="path" direction="out"/>
+    </method>
+
+    <!--
+        Add:
+        @o_path_fd: open file descriptor for the file to add
+        @reuse_existing: whether to reuse an existing document store entry for the file
+        @persistent: whether to add the file only for this session or permanently
+        @doc_id: the ID of the file in the document store
+
+        Adds a file to the document store. The file is passed in the
+        form of an open file descriptor to prove that the caller has
+        access to the file.
+    -->
+    <method name="Add">
+      <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
+      <arg type="h" name="o_path_fd" direction="in"/>
+      <arg type="b" name="reuse_existing" direction="in"/>
+      <arg type="b" name="persistent" direction="in"/>
+      <arg type="s" name="doc_id" direction="out"/>
+    </method>
+
+    <!--
+        AddNamed:
+        @o_path_parent_fd: open file descriptor for the parent directory
+        @filename: the basename for the file
+        @reuse_existing: whether to reuse an existing document store entry for the file
+        @persistent: whether to add the file only for this session or permanently
+        @doc_id: the ID of the file in the document store
+
+        Creates an entry in the document store for writing a new file.
+    -->
+    <method name="AddNamed">
+      <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
+      <arg type="h" name="o_path_parent_fd" direction="in"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QByteArray"/>
+      <arg type="ay" name="filename" direction="in"/>
+      <arg type="b" name="reuse_existing" direction="in"/>
+      <arg type="b" name="persistent" direction="in"/>
+      <arg type="s" name="doc_id" direction="out"/>
+    </method>
+
+    <!--
+        AddFull:
+        @o_path_fds: open file descriptors for the files to export
+        @flags: flags, 1 == ``reuse_existing``, 2 == ``persistent``, 4 == ``as-needed-by-app``, 8 = ``export-directory``
+        @app_id: an application ID, or empty string
+        @permissions: the permissions to grant, possible values are 'read', 'write', 'grant-permissions' and 'delete'
+        @doc_ids: the IDs of the files in the document store
+        @extra_info: Extra info returned
+
+        Adds multiple files to the document store. The file is passed in the
+        form of an open file descriptor to prove that the caller has
+        access to the file.
+
+        If the ``as-needed-by-app`` flag is given, files will only be added to
+        the document store if the application does not already have access to them.
+        For files that are not added to the document store, the @doc_ids array will
+        contain an empty string.
+
+        Additionally, if @app_id is specified, it will be given the permissions
+        listed in org.freedesktop.portal.Documents.GrantPermissions().
+
+        The method also returns some extra info that can be used to avoid
+        multiple roundtrips. For now it only contains as "mountpoint", the
+        fuse mountpoint of the document portal.
+
+        This method was added in version 2 of this interface.
+
+        Support for exporting directories was added in version 4 of this interface.
+    -->
+    <method name="AddFull">
+      <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QList&lt;QDBusUnixFileDescriptor&gt;"/>
+      <arg type="ah" name="o_path_fds" direction="in"/>
+      <arg type="u" name="flags" direction="in"/>
+      <arg type="s" name="app_id" direction="in"/>
+      <arg type="as" name="permissions" direction="in"/>
+      <arg type="as" name="doc_ids" direction="out"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
+      <arg type="a{sv}" name="extra_out" direction="out"/>
+    </method>
+
+    <!--
+        AddNamedFull:
+        @o_path_fds: open file descriptor for the parent directory
+        @filename: the basename for the file
+        @flags: flags, 1 == ``reuse_existing``, 2 == ``persistent``, 4 == ``as-needed-by-app``
+        @app_id: an application ID, or empty string
+        @permissions: the permissions to grant, possible values are 'read', 'write', 'grant-permissions' and 'delete'
+        @doc_id: the ID of the file in the document store
+        @extra_info: Extra info returned
+
+        Creates an entry in the document store for writing a new file.
+
+        If the ``as-needed-by-app`` flag is given, file will only be added to
+        the document store if the application does not already have access to it.
+        For file that is not added to the document store, the @doc_id will
+        contain an empty string.
+
+        Additionally, if @app_id is specified, it will be given the permissions
+        listed in org.freedesktop.portal.Documents.GrantPermissions().
+
+        The method also returns some extra info that can be used to avoid
+        multiple roundtrips. For now it only contains as "mountpoint", the
+        fuse mountpoint of the document portal.
+
+        This method was added in version 3 of this interface.
+    -->
+    <method name="AddNamedFull">
+      <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
+      <arg type="h" name="o_path_fd" direction="in"/>
+      <arg type="ay" name="filename" direction="in"/>
+      <arg type="u" name="flags" direction="in"/>
+      <arg type="s" name="app_id" direction="in"/>
+      <arg type="as" name="permissions" direction="in"/>
+      <arg type="s" name="doc_id" direction="out"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
+      <arg type="a{sv}" name="extra_out" direction="out"/>
+    </method>
+
+    <!--
+        GrantPermissions:
+        @doc_id: the ID of the file in the document store
+        @app_id: the ID of the application to which permissions are granted
+        @permissions: the permissions to grant, possible values are ``read``, ``write``, ``grant-permissions`` and ``delete``
+
+        Grants access permissions for a file in the document store
+        to an application.
+
+        This call is available inside the sandbox if the application
+        has the ``grant-permissions`` permission for the document.
+    -->
+    <method name="GrantPermissions">
+      <arg type="s" name="doc_id" direction="in"/>
+      <arg type="s" name="app_id" direction="in"/>
+      <arg type="as" name="permissions" direction="in"/>
+    </method>
+
+    <!--
+        RevokePermissions:
+        @doc_id: the ID of the file in the document store
+        @app_id: the ID of the application from which permissions are revoked
+        @permissions: the permissions to revoke, possible values are 'read', 'write', 'grant-permissions' and 'delete'
+
+        Revokes access permissions for a file in the document store
+        from an application.
+
+        This call is available inside the sandbox if the application
+        has the ``grant-permissions`` permission for the document.
+    -->
+    <method name="RevokePermissions">
+      <arg type="s" name="doc_id" direction="in"/>
+      <arg type="s" name="app_id" direction="in"/>
+      <arg type="as" name="permissions" direction="in"/>
+    </method>
+
+    <!--
+        Delete:
+        @doc_id: the ID of the file in the document store
+
+        Removes an entry from the document store. The file itself is
+        not deleted.
+
+        This call is available inside the sandbox if the application
+        has the ``delete`` permission for the document.
+    -->
+    <method name="Delete">
+      <arg type="s" name="doc_id" direction="in"/>
+    </method>
+
+    <!--
+        Lookup:
+        @filename: a path in the host filesystem
+        @doc_id: the ID of the file in the document store, or '' if the file is not in the document store
+
+        Looks up the document ID for a file.
+
+        This call is not available inside the sandbox.
+    -->
+    <method name="Lookup">
+      <arg type="ay" name="filename" direction="in"/>
+      <arg type="s" name="doc_id" direction="out"/>
+    </method>
+
+    <!--
+        Info:
+        @doc_id: the ID of the file in the document store
+        @path: the path for the file in the host filesystem
+        @apps: a dictionary mapping application IDs to the permissions for that application
+
+        Gets the filesystem path and application permissions for a document store
+        entry.
+
+        This call is not available inside the sandbox.
+    -->
+    <method name="Info">
+      <arg type="s" name="doc_id" direction="in"/>
+      <arg type="ay" name="path" direction="out"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QMap&lt;QString,QStringList&gt;"/>
+      <arg type="a{sas}" name="apps" direction="out"/>
+    </method>
+
+    <!--
+        List:
+        @app_id: an application ID, or '' to list all documents
+        @docs: a dictionary mapping document IDs to their filesystem path
+
+        Lists documents in the document store for an application (or for
+        all applications).
+
+        This call is not available inside the sandbox.
+    -->
+    <method name="List">
+      <arg type="s" name="app_id" direction="in"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QMap&lt;QString,QByteArray&gt;"/>
+      <arg type="a{say}" name="docs" direction="out"/>
+    </method>
+
+    <!--
+        GetHostPaths:
+        @doc_ids: the list of IDs of the files in the document store
+        @paths: a dictionary mapping document IDs to the paths in the host filesystem
+
+        Gets the host filesystem paths for document store entries.
+
+        The host filesystem path is also available in the ``user.document-portal.host-path`` extended file attribute.
+
+        This call is available inside the sandbox, if the application has the ``read`` permission for the documents.
+
+        This method was added in version 5 of this interface.
+    -->
+    <method name="GetHostPaths">
+      <arg type="as" name="doc_ids" direction="in"/>
+      <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QMap&lt;QString,QByteArray&gt;"/>
+      <arg type="a{say}" name="paths" direction="out"/>
+    </method>
+  </interface>
+</node>
diff --git a/src/xdp-documents.c b/src/xdp-documents.c
new file mode 100644
index 0000000..288b9e4
--- /dev/null
+++ b/src/xdp-documents.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2016 Endless Mobile, Inc.
+ * Copyright 2025 Canonical Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "xdp-documents.h"
+
+#include <fcntl.h>
+#include <glib/gstdio.h>
+#include <gio/gdesktopappinfo.h>
+
+#include "xdp-documents-generated.h"
+
+/* Flags accepted by org.freedesktop.portal.Documents.AddFull */
+enum {
+	XDP_ADD_FLAGS_REUSE_EXISTING             =  (1 << 0),
+	XDP_ADD_FLAGS_PERSISTENT                 =  (1 << 1),
+	XDP_ADD_FLAGS_AS_NEEDED_BY_APP           =  (1 << 2),
+	XDP_ADD_FLAGS_FLAGS_ALL                  = ((1 << 3) - 1)
+} G_GNUC_FLAG_ENUM;
+
+
+char *
+get_sandboxed_app_id (GAppInfo *app_info)
+{
+	char *app_id;
+	GDesktopAppInfo *info;
+
+	if (!G_IS_DESKTOP_APP_INFO (app_info))
+		return NULL;
+
+	info = G_DESKTOP_APP_INFO (app_info);
+	app_id = g_desktop_app_info_get_string (info, "X-Flatpak");
+	if (!app_id)
+	{
+		char *snap_instance;
+
+		snap_instance = g_desktop_app_info_get_string (info, "X-SnapInstanceName");
+
+		if (snap_instance && *snap_instance)
+			app_id = g_strconcat ("snap.", snap_instance, NULL);
+
+		g_free (snap_instance);
+	}
+
+	return app_id;
+}
+
+GList *
+document_portal_add_documents (GList        *uris,
+                               const char   *app_id,
+                               GError      **error)
+{
+	g_autoptr (XdpDocuments) proxy = NULL;
+	g_autofree char *documents_mountpoint = NULL;
+	GList *ruris = NULL;
+	g_auto (GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("ah"));
+	g_autoptr (GUnixFDList) fd_list = NULL;
+	GList *l;
+	gsize i;
+	const char *permissions[] = { "read", NULL };
+	g_auto (GStrv) doc_ids = NULL;
+
+	proxy = xdp_documents_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+	                                                      G_DBUS_PROXY_FLAGS_NONE,
+	                                                      "org.freedesktop.portal.Documents",
+	                                                      "/org/freedesktop/portal/documents",
+	                                                      NULL,
+	                                                      error);
+
+	if (!proxy)
+		return NULL;
+
+	if (!xdp_documents_call_get_mount_point_sync (proxy,
+	                                              &documents_mountpoint,
+	                                              NULL,
+	                                              error))
+		return NULL;
+
+	fd_list = g_unix_fd_list_new ();
+	for (l = uris, i = 0; l; l = l->next, i++) {
+		const char *uri = l->data;
+		g_autofree char *path = NULL;
+		int fd;
+
+		path = g_filename_from_uri (uri, NULL, error);
+		if (!path)
+			return NULL;
+
+		fd = g_open (path, O_CLOEXEC | O_RDONLY);
+		if (fd < 0) {
+			g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
+			             "Could not open '%s' for reading: %s",
+			             path, g_strerror (errno));
+			return NULL;
+		}
+
+		int idx = g_unix_fd_list_append (fd_list, fd, NULL);
+		g_variant_builder_add (&builder, "h", idx);
+		close (fd);
+	}
+
+	if (!xdp_documents_call_add_full_sync (proxy,
+	                                       g_variant_builder_end (&builder),
+	                                       XDP_ADD_FLAGS_AS_NEEDED_BY_APP,
+	                                       app_id,
+	                                       permissions,
+	                                       fd_list,
+	                                       &doc_ids,
+	                                       NULL,
+	                                       NULL,
+	                                       NULL,
+	                                       error))
+		return NULL;
+
+	for (l = uris, i = 0; l; l = l->next, i++) {
+		const char *uri = l->data;
+		char *ruri;
+
+		if (strcmp (doc_ids[i], "") == 0) {
+			ruri = g_strdup (uri);
+		} else {
+			const char *doc_name;
+			g_autofree char *doc_path;
+			g_autofree char *filename = NULL;
+			g_autoptr (GDir) doc_dir = NULL;
+			g_autoptr (GError) local_error = NULL;
+
+			doc_path = g_build_filename (documents_mountpoint, doc_ids[i], NULL);
+			doc_dir = g_dir_open (doc_path, 0, &local_error);
+
+			if (!doc_dir) {
+				g_set_error_literal (error, G_IO_ERROR,
+				                     g_io_error_from_file_error (local_error->code),
+				                     local_error->message);
+				g_clear_list (&ruris, g_free);
+				return NULL;
+			}
+
+			doc_name = g_dir_read_name (doc_dir);
+			if (!doc_name) {
+				g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+				             "Failed to read %s" G_DIR_SEPARATOR_S "%s",
+				             documents_mountpoint, doc_ids[i]);
+				g_clear_list (&ruris, g_free);
+				return NULL;
+			}
+
+			filename = g_build_filename (documents_mountpoint, doc_ids[i], doc_name, NULL);
+			ruri = g_filename_to_uri (filename, NULL, error);
+			if (!ruri)
+				return NULL;
+		}
+
+		ruris = g_list_prepend (ruris, ruri);
+	}
+
+	return g_list_reverse (ruris);
+}
\ No newline at end of file
diff --git a/src/xdp-documents.h b/src/xdp-documents.h
new file mode 100644
index 0000000..5d71aa4
--- /dev/null
+++ b/src/xdp-documents.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2025 Canonical Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include <gio/gio.h>
+
+GList *
+document_portal_add_documents (GList        *uris,
+                               const char   *app_id,
+                               GError      **error);
+
+char *
+get_sandboxed_app_id (GAppInfo *app_info);
