# Platform architecture Flectar Mail is one shared application with thin, per-platform hosts. | Layer | Location | Contains | | --- | --- | --- | | Core | `crates/flectar-mail-core` | Protocols, sync, storage, search. No UI and no host APIs. | | Application | `src/`, `ui/` | Slint UI, controllers, rendering. Shared by every platform. | | Platform layer | `src/platform/` | The application's only code that names a mobile operating system. | | Hosts | `platform//` | Native entry points, packaging and native services (JNI, Objective-C). | Desktop builds use the `flectar-mail` binary (`src/desktop.rs`), and the release workflows package it for Linux, Windows and macOS. Mobile hosts embed the same library. ## Capability aliases Shared code never asks which operating system it runs on, only what the platform provides. `build.rs` and `crates/flectar-mail-core/build.rs` define these aliases with [`cfg_aliases`](https://crates.io/crates/cfg_aliases): | Alias | Meaning | | --- | --- | | `mobile` | A host-managed sandbox: no file dialogs, tray, GnuPG, desktop renderer choice or system keychain access from the core. | | `desktop` | `not(mobile)` | | `linux_desktop` | A Linux desktop session (portals, X11/Wayland, Secret Service). Excludes OpenHarmony, which also reports `target_os = "linux"`. | | `glibc` | Linux with the GNU C library. | | `rename_noreplace` | Core only: kernels with an atomic no-replace directory rename. | | `secret_service` | Core only: desktops where `keyring` stores secrets through the freedesktop Secret Service (Linux, FreeBSD and OpenBSD, not OpenHarmony). | Use them like any other `cfg`: `#[cfg(desktop)]`, `cfg!(mobile)`. Cargo cannot evaluate build-script aliases in `[target.'cfg(...)']` tables, so the manifests repeat the operating-system lists there with a comment naming the alias. Keep the two in step. `scripts/check-platform-gates.py` runs in CI and rejects `target_os = "android"`, `"ios"` or `target_env = "ohos"` outside the build scripts and `src/platform/`, and a bare `target_os = "linux"` anywhere in the application. Windows and macOS conditions are allowed where code really is specific to them, such as native window chrome or keyboard shortcut conventions. ## Host contract A host runs the shared application by building a `PlatformContext` and calling `flectar_mail::run` (or `run_desktop`). The context carries every host-provided service: | Field | Desktop | Mobile host | | --- | --- | --- | | `paths` | `Paths::default_dirs()` | App-private data and cache directories | | `documents` | Native file dialogs | Implements `documents::DocumentProvider` with the system picker | | `credentials` | System keychain (`SystemCredentialStore`) | Implements `CredentialStore` with the platform keystore | | `oauth_redirects` | Loopback redirect broker | Implements `OAuthRedirectBroker` (app links, custom schemes) | | `external_urls` | System URL launcher | `ExternalUrlOpener::new` with the host's native URL bridge | | `notifications` | `None`; `run` creates the platform's own (toasts, User Notifications, the freedesktop server or the portal) | A `NotificationHost` that shows, withdraws and reports on notifications, or `None` where the host has none. The shared app decides what to show, sums up bursts and translates the text; the host lays each notification out, says per notification whether the system took it, and reports clicks and buttons through `respond` | Mobile hosts use `PlatformContext::app_private(data_root, cache_dir, documents, credentials, oauth_redirects)`, which keeps durable data below `data_root/data`. A host without secure storage yet must pass `UnavailableCredentialStore` so account setup reports the gap. Never fall back to a plaintext store. The constructors default `external_urls` to the system URL launcher. A host without desktop launchers configures its context before calling `run`: ```rust,ignore platform.external_urls = flectar_mail::ExternalUrlOpener::new(bridge::open_url); ``` This replaces the process-global `set_external_url_opener` registration. The callback may capture host state; cloned contexts share it. Mail links invoke it on the application event loop, and browser-based OAuth (including retries) invokes it on a worker thread. The host must marshal calls onto any thread its native API requires and return promptly after submitting the request. Errors reach the UI without falling back to a desktop launcher. Email links retain their `http`/`https`/`mailto`/`tel` allowlist, while browser authorization requires a parsed HTTPS URL with a host. The existing in-app `mailto` compose route and native OAuth broker flows remain in place. Each mobile operating system also has a module in `src/platform/` that provides these crate-internal items, which shared code calls without a `cfg`: | Item | Purpose | | --- | --- | | `select_renderer()` | Install the Slint backend if the host has not, and return the renderer name for diagnostics. | | `pdfium_library()` | Path of the bundled PDFium library. | | `lower_current_thread_priority()` | Run the PDF worker thread at background priority. | | `install_preview_lifecycle()` | Register for background/memory-pressure notifications, which must call `flectar_mail::suspend_file_preview()`. | | `NAME` | The platform's name, which Settings uses to describe where the app icon appears. | | `SWITCHES_APP_ICON` | Whether apps may switch their home screen icon. Settings offers the choice only where they may. | | `show_app_icon(icon)` | Show the app icon chosen in Settings on the home screen. Android forwards it to the bridge its host registers with `configure_launcher_icon`; iOS switches to the alternate icon sets declared in `platform/ios/project.yml`. | | `email_fonts()` | Platform font directory and fallback policy for the email (Blitz/Parley) renderer and the composer on hosts without Fontconfig; `None` when the fontique system backend provides the fonts. The OpenHarmony host supplies its backend's policy with `configure_email_fonts`. | Hosts that launch through `src/desktop.rs` (desktop and iOS) also provide `platform::launch_context()`. `DocumentProvider::export()` receives files from two app-private places: generated files below the cache directory, and database snapshots, which are staged as one zip in `snapshot-staging` beside the data directory because Core refuses to write snapshots inside the data and cache roots. A host that restricts export sources must accept both. Shared code starts every picker and export through `documents::run_request`, which runs the provider future on the Tokio runtime while the UI event loop keeps going. A host that needs another threading policy while its picker is up changes that one function. ### Known gaps - Mobile document transfers are capped at 512 MiB, so Android and iOS cannot export database snapshots of larger mailboxes yet. The export fails before the picker opens. ## Adding a platform 1. Add the target to the `mobile` alias in both build scripts, if it is a mobile host, and to the matching `[target.'cfg(...)']` lists in both `Cargo.toml` files. 2. Add `src/platform/.rs` with the items above and re-export it from `src/platform/mod.rs`. 3. Add `platform//` with the native entry point, packaging, and the native implementations of the `PlatformContext` services. Hosts with their own Cargo workspace repeat the root `[patch.crates-io]` entries and use `default-features = false`. 4. Run `python3 scripts/check-platform-gates.py` and the lint for the new target. If any shared code still needs changes, the platform needs a new capability alias, not a new operating-system condition at the call site. 5. Document the build in `platform//README.md` and add a CI job for the platform's support tier. ## Support tiers | Tier | Platforms | Expectations | | --- | --- | --- | | 1 | Linux, Windows, macOS | Release packages. Lint and tests pass on every pull request. | | 2 | Android, iOS | Experimental builds. Lint passes on every pull request; a broken build blocks merging. | | 3 | OpenHarmony | In-tree with a named maintainer listed in the host README. Built by a manually dispatched workflow, not on pull requests. A port that stays broken while its maintainer can't be reached is marked unmaintained, and is removed only if nobody takes it over. | New platforms start at tier 3. A maintainer may promote a platform once it has been kept green for several releases and has an owner for its release builds. ## Signing and secrets Release signing material and provider client secrets exist only as GitHub secrets used by manually dispatched or release workflows, never in pull request workflows or the repository. Pull request checks build unsigned. ## Vendored dependencies Prefer upstream fixes. When a platform needs a patched dependency before upstream releases it, follow the existing format in `patches//`: a README explaining the change and its upstream link, the pinned upstream revision and checksums, and the patch series. Add a verification step to `.github/workflows/quality.yml`, and remove the vendored copy once upstream ships the fix.