Files

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/<name>/ 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:

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.
user_notifications New-mail notifications through Apple's User Notifications framework (macOS, iOS).
background_checks The system suspends the app in the background and wakes it to check for new mail (iOS, Android).
background_engine The system may start the app without its window, for a check for new mail or a button on a notification (Android).
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, or respond_then where the system suspends the app soon after. Where the system wakes the app to check for mail (background_checks), the host calls check_in_background when it does and app_became_active when the app comes to the front
spelling None; the app checks with the system's dictionaries or spell checker itself A SpellingHost where the app has no checker of its own, or None. Android's host brings the spell checker chosen in the system's settings, which checks in its own process. The app opens a SpellingSession at a message's first check, calls it off the UI thread with the text in pieces, and drops it when the last message closes. iOS needs none, since the app checks with UITextChecker itself

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:

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/<name>.rs with the items above and re-export it from src/platform/mod.rs.
  3. Add platform/<name>/ 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/<name>/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/<crate>/: 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.