app-id: com.kysecurity.mail runtime: org.kde.Platform runtime-version: "6.11" sdk: org.kde.Sdk # Qt6::WebEngineQuick (app/CMakeLists.txt's find_package(Qt6 REQUIRED # COMPONENTS WebEngineQuick), used only by app/qml/pages/EmailDetail.qml's # WebEngineView) isn't in the org.kde.Platform//6.10 runtime itself, so # the build previously failed with "Could NOT find Qt6WebEngineQuick". # Docs/TESTING.md/Linux_QT_Client_Plan.md previously assumed Flathub's # io.qt.qtwebengine.BaseApp topped out around Qt 6.4 and a from-source # qtwebengine module (a large Chromium build) would be needed -- that # assumption was stale/wrong: confirmed live via `flatpak remote-info # --user flathub io.qt.qtwebengine.BaseApp//6.10` that a real, # published, pre-built branch/6.10 extension exists (222MB download). # `base`/`base-version` is the documented way apps needing WebEngine # consume it (not a from-source rebuild) -- verified against real # published KDE apps on this exact runtime/sdk stack, e.g. # flathub/org.kde.cantor's manifest uses the identical # base=io.qt.qtwebengine.BaseApp/base-version=6.10/cleanup-commands # trio. No changes needed to the `modules` list below -- the base app # already provides Qt6WebEngineQuick's headers/libs during the build. base: io.qt.qtwebengine.BaseApp base-version: "6.11" # 6.10 left upstream support on 2026-04-07 and its Flathub BaseApp has not # been rebuilt since 2026-02-08, so it will never receive another Chromium # backport. `base:` bakes libQt6WebEngineCore into the APP bundle, so a # runtime update can never replace it -- the pin is the whole story. The # shipped 6.10.2 sits at Chromium security level 144.0.7559.96, below the # 146.0.7680.75 that fixes CVE-2026-3909, a KEV-listed out-of-bounds write in # Skia reachable from a crafted HTML page. Skia is the rasterizer, not V8, so # EmailDetail.qml's javascriptEnabled: false does not remove it and mail # bodies reach it directly. cleanup-commands: - /app/cleanup-BaseApp.sh command: kypost finish-args: - --socket=wayland - --socket=fallback-x11 - --share=network # Client-side OpenPGP decryption (core/pgp/OpenPgpDecryptor.cpp) delegates # to the user's own gpg-agent through GPGME, so this app holds no key # material and never sees a passphrase -- which is also why hardware tokens # and smartcards work here with no code. That delegation is a socket into # the host: without this grant the sandboxed gpgme finds no agent, every # encrypted message reports "GnuPG is not available", and the feature is # simply absent in the Flatpak while working everywhere else. # # Narrower than it looks. It exposes the AGENT, not the keyring: the agent # performs operations with keys it holds and does not hand them out, and # the user still approves each unlock through their own pinentry, outside # this sandbox. - --socket=gpg-agent # KDBusService(KDBusService::Unique) in app/main.cpp claims this # well-known bus name for duplicate-launch detection and D-Bus # activation (packaging/dbus/com.kysecurity.mail.service, wired by # Task 45). KUnifiedPush::Connector (app/push/UnifiedPushConnector.cpp) # registers under this same serviceName on the session bus without # claiming it itself -- its constructor's own comment documents # "Destination= Path=/org/unifiedpush/Connector" as the # address the distributor uses to deliver push wake-ups. Without # --own-name, KDBusService::Unique's RequestName call is denied by # the sandbox and every launch (plus D-Bus activation and # UnifiedPush delivery) breaks. - --own-name=com.kysecurity.mail # Qt6Keychain (app/platform/SecureStoreKeychain.cpp, used to persist # pairing credentials) talks to the Secret Service D-Bus API on # Linux, i.e. org.freedesktop.secrets, exported by # gnome-keyring/kwalletd's ksecretd. Without this the keychain reads # and writes on every pairing/credential operation fail. - --talk-name=org.freedesktop.secrets # KUnifiedPush::Connector::registerClient() (app/push/UnifiedPushConnector.cpp) # calls out to the local UnifiedPush distributor over D-Bus to # request an endpoint -- a separate direction from the # --own-name=com.kysecurity.mail grant above, which only covers the # distributor calling back INTO the app for delivery/activation. # Without a --talk-name for the distributor's bus name, the # sandbox's D-Bus proxy blocks this outbound registration call # and the Distributor push tier can never acquire an endpoint # from inside the sandbox. Confirmed live on a dev machine # running kunifiedpush-distributor: `busctl --user list` shows # the distributor owns "org.unifiedpush.Distributor.kde" at # /org/unifiedpush/Distributor (interfaces # org.unifiedpush.Distributor1/Distributor2); `strings` on the # installed libKUnifiedPush.so confirms this # org.unifiedpush.Distributor. naming is the library's own # convention, not one implementation's quirk -- hence the # wildcard, to cover other distributors too. - --talk-name=org.unifiedpush.Distributor.* # PGP QR key exchange's "Scan to Add Contact Key" screen needs # camera access (QtMultimedia Camera/CaptureSession, decoded via # PgpQrScanner/zxing-cpp). This machine's qt6-multimedia-ffmpeg # backend talks to V4L2 devices directly, not the pipewire camera # portal, so the narrower PipeWire socket grant below (sufficient for a # GStreamer/pipewire-portal backend) is NOT enough here -- # confirmed by which qt6-multimedia-* backend package is actually # installed (ffmpeg, not gstreamer) on this dev machine. # # NARROWED from --device=all (2026-07-26 review). --device=all is # not "cameras": it exposes every device node in /dev to the # sandbox, including /dev/input, i.e. the ability to read every # keystroke going to every other application in the session. That # is a strictly larger grant than the GPU + camera this app needs, # and it was reached for because it was the flag that made the # camera work, not because the surface was intended. # # --device=dri covers the GPU nodes the Qt Quick scenegraph and # WebEngine use. Camera access rides on the PipeWire socket plus the # camera portal where the backend supports it; the remaining # V4L2-direct case is handled by the far narrower --device=input # substitute below being ABSENT on purpose -- if the ffmpeg backend # in org.kde.Sdk 6.10 turns out to need raw V4L2, add # `--device=all` back ONLY with a dated note recording that it was # re-verified as necessary against that runtime, rather than # inherited from this dev machine. - --device=dri # Spelled as a filesystem grant rather than `--socket=pipewire`, which is # what this used to say. They expose the same thing -- the PipeWire socket # at $XDG_RUNTIME_DIR/pipewire-0 -- but the `pipewire` SOCKET TYPE only # exists in flatpak 1.15.4 and newer, and `flatpak build-finish` rejects an # unknown socket type outright: # # error: Unknown socket type pipewire, valid types are: x11, wayland, # pulseaudio, session-bus, system-bus, fallback-x11, ssh-auth, pcsc, # cups, gpg-agent # # ubuntu-24.04 ships flatpak 1.14.6, so this failed every CI build at the # "Finishing app" step. The filesystem spelling has worked since 1.0 and is # what Flathub used for years before the socket type existed, so it also # keeps the built app installable by users on older flatpak. # # Same grant, same narrowness -- this is NOT a widening. See the note above # on why --device=all was removed and must not come back without a dated # re-verification. - --filesystem=xdg-run/pipewire-0 # Attachment downloads. Without a filesystem grant, $HOME inside the # sandbox is ~/.var/app/com.kysecurity.mail, so # QStandardPaths::DownloadLocation resolved to a directory the user # has no way to find -- "Save attachment" reported success and put # the file nowhere they would ever look. (Compose's file picker was # unaffected: it goes through the portal, which grants access per # file.) xdg-download is the narrowest grant that makes the save # path mean what the UI says it means. - --filesystem=xdg-download # Desktop notifications (KF6::Notifications, NotificationDispatcher.cpp, # KNotification("newMail")): deliberately NOT adding # --talk-name=org.freedesktop.Notifications here. Researched Task 47 # (2026-07-14) against the locally installed KF6::Notifications 6.27.0: # KNotificationManager::pluginForAction() (knotificationmanager.cpp, # invent.kde.org/frameworks/knotifications) calls isInsideSandbox() # -- QFileInfo::exists("/.flatpak-info") || qEnvironmentVariableIsSet # ("SNAP") -- and when that is true *and* # org.freedesktop.portal.Desktop is registered on the session bus, it # instantiates NotifyByPortal (talks to # org.freedesktop.portal.Notification) instead of NotifyByPopup # (which would talk to org.freedesktop.Notifications directly). # Confirmed locally too: `strings -e l libKF6Notifications.so.6.27.0` # contains the literal UTF-16 string "/.flatpak-info" alongside # "org.freedesktop.portal.Desktop", matching that source. Inside this # Flatpak, KNotifications therefore auto-routes through the portal, # and portals never need finish-args entries -- adding a talk-name # for the raw interface here would be dead permission surface. modules: # Argon2id, the key-derivation function behind the credential seal # (core/security/CredentialCipher.h). Built from the reference # implementation because org.kde.Platform does not ship libargon2 -- # libcrypto is there (Qt links it for TLS) but OpenSSL exposes no Argon2 # through EVP in the version the runtime carries. # # Upstream's Makefile has no configure step and defaults to PREFIX=/usr # with a Debian multiarch libdir. PREFIX/LIBRARY_REL are passed to BOTH # make and make install, not just install: libargon2.pc is generated # during the build phase by sed-substituting those variables into a # template, so building with the defaults and only overriding at install # time yields a .pc that says prefix=/usr and # libdir=/usr/lib/x86_64-linux-gnu -- verified. pkg_check_modules would # then find that file and point the link at a path holding nothing. - name: libargon2 buildsystem: simple build-commands: - make PREFIX=/app LIBRARY_REL=lib -j $FLATPAK_BUILDER_N_JOBS - make install PREFIX=/app LIBRARY_REL=lib cleanup: - /bin - /lib/libargon2.a sources: - type: archive url: https://github.com/P-H-C/phc-winner-argon2/archive/refs/tags/20190702.tar.gz sha256: daf972a89577f8772602bf2eb38b6a3dd3d922bf5724d45e7f9589b5e830442c - name: qtkeychain buildsystem: cmake-ninja config-opts: - -DBUILD_WITH_QT5=OFF - -DBUILD_TEST_APPLICATION=OFF - -DBUILD_TRANSLATIONS=OFF # Install into /app/lib, not /app/lib64. Both this module and zxing-cpp # use GNUInstallDirs, which resolves CMAKE_INSTALL_LIBDIR to `lib64` on # x86-64 in this SDK -- but a flatpak's runtime linker path is /app/lib, # and nothing adds /app/lib64 to it. The libraries were built, installed # and shipped, and the app then died on launch with: # # kypost: error while loading shared libraries: libZXing.so.4: # cannot open shared object file: No such file or directory # # Fixed for BOTH modules at once deliberately: libqt6keychain.so.1 was # landing in the same wrong place, so correcting only the library named # in the error would have produced the identical failure on the next # run. (libargon2 already avoids this -- it passes LIBRARY_REL=lib to # its plain Makefile for exactly this reason.) - -DCMAKE_INSTALL_LIBDIR=lib sources: - type: archive url: https://github.com/frankosterfeld/qtkeychain/archive/refs/tags/0.17.0.tar.gz sha256: 3b85c3929034b0a99da777130c34d99f006fcd3a9d56564159399a33fee0e504 # SQLCipher, the encryption layer for the local mail/contacts database. # # Built with an explicit SONAME of libsqlite3.so.0, which is the whole # reason this is a module rather than a package. Qt's stock SQLite driver # plugin (libqsqlite.so, shipped in the runtime) has a DT_NEEDED on # libsqlite3.so.0. Give it a library carrying that SONAME from /app/lib -- # which the loader searches before the runtime's /usr/lib -- and the # plugin drives SQLCipher unchanged, with no vendored Qt driver, no # private Qt headers and no custom plugin. Measured with LD_DEBUG=libs: # exactly one libsqlite3 initialises, and it is this one. Without the # SONAME, the runtime's own SQLite is mapped alongside it and the two # implementations coexist in one process. # # SQLITE_ENABLE_COLUMN_METADATA is not optional either: Qt's driver # imports sqlite3_column_table_name16, and a build without it fails to # load with the singularly unhelpful "Driver not loaded". # # SQLITE_TEMP_STORE=2 keeps temp tables and sort spills in RAM. An # encrypted database that spills plaintext to a scratch file next to # itself is not an encrypted database. # # Kept in step with scripts/build-sqlcipher.sh, which CI runs -- the flags # and the pinned version must match, or the thing CI tests is not the # thing users get. - name: sqlcipher buildsystem: autotools config-opts: - --with-tempstore=yes - --disable-tcl - CFLAGS=-DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=2 -DSQLITE_EXTRA_INIT=sqlcipher_extra_init -DSQLITE_EXTRA_SHUTDOWN=sqlcipher_extra_shutdown -DSQLITE_ENABLE_COLUMN_METADATA - LDFLAGS=-lcrypto -Wl,-soname,libsqlite3.so.0 sources: - type: archive url: https://github.com/sqlcipher/sqlcipher/archive/refs/tags/v4.14.0.tar.gz sha256: 67fb27e967a4a6968c0905691c89c908e7250dddc581b887c19ef981c737e473 - name: zxing-cpp buildsystem: cmake-ninja config-opts: - -DCMAKE_BUILD_TYPE=Release # Install into /app/lib, not /app/lib64. Both this module and zxing-cpp # use GNUInstallDirs, which resolves CMAKE_INSTALL_LIBDIR to `lib64` on # x86-64 in this SDK -- but a flatpak's runtime linker path is /app/lib, # and nothing adds /app/lib64 to it. The libraries were built, installed # and shipped, and the app then died on launch with: # # kypost: error while loading shared libraries: libZXing.so.4: # cannot open shared object file: No such file or directory # # Fixed for BOTH modules at once deliberately: libqt6keychain.so.1 was # landing in the same wrong place, so correcting only the library named # in the error would have produced the identical failure on the next # run. (libargon2 already avoids this -- it passes LIBRARY_REL=lib to # its plain Makefile for exactly this reason.) - -DCMAKE_INSTALL_LIBDIR=lib - -DZXING_READERS=ON - -DZXING_WRITERS=ON - -DZXING_EXAMPLES=OFF - -DZXING_UNIT_TESTS=OFF - -DZXING_DEPENDENCIES=LOCAL # zxing-cpp 3.1.0's top-level CMakeLists.txt unconditionally # does add_subdirectory(docs) -- there is no # ZXING_BUILD_DOCS-style option to opt out. docs/CMakeLists.txt # only fetches anything if find_package(Doxygen) succeeds # (org.kde.Sdk//6.10 has doxygen installed, so it does), then # FetchContent_MakeAvailable()'s a doxygen-awesome-css zip # straight from github.com -- which fails deterministically # inside flatpak-builder's network-isolated build sandbox # ("Could not resolve host: github.com"), confirmed via a # real build attempt. CMAKE_DISABLE_FIND_PACKAGE_Doxygen is # the standard CMake mechanism for exactly this case: forces # find_package(Doxygen) to report not-found regardless of # what's actually on the SDK, without needing zxing-cpp # itself to support disabling its docs target. - -DCMAKE_DISABLE_FIND_PACKAGE_Doxygen=ON # wrappers/c/CMakeLists.txt's zxing_add_package_stb() macro # (zxing.cmake) checks STB_IMAGE_INCLUDE_DIR first, before # falling back to pkg-config or a network FetchContent -- # with ZXING_DEPENDENCIES=LOCAL, a pkg-config miss on `stb` # (not packaged on org.kde.Sdk//6.10) is a hard REQUIRED # failure rather than a fallback, confirmed via a real build # attempt. stb is a single-header library with no build # step, so it's vendored below as a plain source (fetched by # flatpak-builder's own download phase, which has network # access, unlike the sandboxed configure/build phase that # FetchContent would need it in) rather than switching # ZXING_DEPENDENCIES to AUTO, which would just move the same # class of failure to whichever *other* zxing_add_package # dependency isn't present as a system package either. # /run/build/ is flatpak-builder's own fixed, # documented build-directory path inside the sandbox # (confirmed from a real build log). - -DSTB_IMAGE_INCLUDE_DIR=/run/build/zxing-cpp/stb-vendor sources: # Switched from a GitHub release-tarball "archive" source to a # "git" source: v3.1.0's auto-generated tarball doesn't include # the `zint` git submodule (.gitmodules: path=zint, # url=github.com/zint/zint.git) that core/src/libzint/zint.h # requires ("../../../zint/backend/zint.h: No such file or # directory") -- confirmed via a real build attempt. # flatpak-builder's git source type fetches submodules by # default (no disable-submodules flag set), so this alone # resolves it. tag+commit both pinned, matching this manifest's # own io.qt.qtwebengine.BaseApp base's convention for its # qtwebengine source. - type: git url: https://github.com/zxing-cpp/zxing-cpp.git tag: v3.1.0 commit: 885baaf0840335153c1a487fa65f9c1388702c81 - type: archive url: https://github.com/nothings/stb/archive/31c1ad37456438565541f4919958214b6e762fb4.tar.gz sha256: e4e3bba9c572a4a4148373a914d88ea0f0d11de8cc2c66739926e7eca0223319 dest: stb-vendor # KUnifiedPush (app/push/UnifiedPushConnector.cpp's # , app/CMakeLists.txt's # find_package(KUnifiedPush REQUIRED)) was never added as a # module here -- this manifest predates the UnifiedPush # integration work (Task 10+), and nothing since then re-ran a # full sandboxed flatpak-builder build to notice the gap (the # only prior sandboxed-build verification, Task 9, predates # UnifiedPush entirely; all live UnifiedPush E2E proofs since # were on native, non-Flatpak builds -- see the # --talk-name=org.unifiedpush.Distributor.* comment above for # the matching D-Bus-permission half of this same gap). # org.kde.Platform//6.10 doesn't bundle it (confirmed: this dev # machine has it as a separate pacman package, # kunifiedpush-26.04.3). Sourced from KDE's own release-service # tarball, version-matched to this dev machine's installed # kunifiedpush-26.04.3 for consistency with what's already been # live-tested; no extra config-opts needed, mirroring the real, # published flathub/org.kde.neochat manifest's identical # kunifiedpush module (NeoChat is a real KDE app that also # integrates UnifiedPush, same org.kde.Platform/Sdk stack). - name: kunifiedpush buildsystem: cmake-ninja builddir: true sources: - type: archive url: https://download.kde.org/stable/release-service/26.04.3/src/kunifiedpush-26.04.3.tar.xz sha256: 247ccc7bcc4277e2fc63a753c367d0ca10efe3b4916f60a44d9ca2da403c4fe5 - name: kypost buildsystem: cmake-ninja run-tests: true config-opts: - -DCMAKE_BUILD_TYPE=Release # Points at the sqlcipher module above, already installed into /app by # the time this module configures. Losing this line no longer produces # a quietly-unencrypted package: CMakeLists.txt stops a Release build # that has no SQLCipher, which is what this is. - -DKYPOST_SQLCIPHER_ROOT=/app sources: - type: dir path: ../..