docs(play): add privacy policy, data-safety mapping, and permissions justification

Repo-actionable deliverables for the Google Play compliance work (issue #17),
every claim verified against the code and the built release artifacts:

- PRIVACY.md: user-facing privacy policy (device-local mail cache, optional
  SQLCipher encryption, traffic only to the user's own mail provider,
  on-device-only contacts autocomplete, strictly local opt-in debug reports,
  no ads/analytics/tracking SDKs).
- docs/play-data-safety.md: Play Data safety questionnaire mapping -- answer
  'no data collected/shared' with per-category code evidence, the policy
  exemptions relied on, a dependency audit, and a conservative fallback.
- docs/play-permissions.md: merged-manifest permission audit (incl. the
  WorkManager-injected WAKE_LOCK / RECEIVE_BOOT_COMPLETED) with paste-ready
  Console justifications for READ_CONTACTS, POST_NOTIFICATIONS, and the
  FOREGROUND_SERVICE_DATA_SYNC declaration + demo-video script.
- docs/play-compliance.md: verified targetSdk 37 (requirement: 35+), 16 KB
  page-size compliance (all packaged .so PT_LOAD p_align=0x4000, incl.
  sqlcipher-android 4.16.0), bundleRelease AAB check, the Gmail-app-password /
  no-CASA OAuth note, the console-steps checklist with drafted content-rating
  and listing answers, and repo findings (push-mail default vs docs, README
  minSdk/app-lock drift, debug-key release fallback).
- README.md: link PRIVACY.md and note the no-Google-OAuth/no-CASA status
  (fuller README pass stays issue #20).

Part of #17.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 21:52:40 -05:00
co-authored by Claude Fable 5
parent 0880dd7f54
commit 3f7024d05d
5 changed files with 514 additions and 2 deletions
+152
View File
@@ -0,0 +1,152 @@
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
# Google Play technical compliance & console checklist (issue #17)
Verified 2026-07-01 against this repository (commit on `main` at time of writing). Companion
docs: [`PRIVACY.md`](../PRIVACY.md), [`play-data-safety.md`](play-data-safety.md),
[`play-permissions.md`](play-permissions.md).
## 1. Target API level — PASS
| Fact | Value | Source |
|---|---|---|
| `targetSdk` | **37** | `app/build.gradle.kts:52` |
| `compileSdk` | 37 | `app/build.gradle.kts:46` |
| `minSdk` | 29 (Android 10) | `app/build.gradle.kts:51` |
| Play requirement (new apps & updates, phones/tablets) | target API **35** (Android 15)+ since 2025-08-31 | [Play target-API policy](https://support.google.com/googleplay/android-developer/answer/11926878) |
Target 37 exceeds the requirement with two versions of headroom; no action needed. When Google
announces the 2026 deadline (expected: API 36 for the Aug 2026 window), 37 still passes.
## 2. 16 KB page-size support — PASS (verified empirically)
Play requires new apps and updates targeting Android 15+ to support 16 KB memory page sizes on
64-bit devices since 2025-11-01 ([Android developers blog](https://android-developers.googleblog.com/2025/05/prepare-play-apps-for-devices-with-16kb-page-size.html)).
Compliance = every `PT_LOAD` segment of every packaged 64-bit `.so` aligned to ≥ 0x4000 (16384).
The release AAB packages exactly three native libraries. All were extracted from
`app-release.aab` and their ELF program headers checked (same check as AOSP's
`check_elf_alignment.sh`); **every one reports `p_align = 0x4000` on every ABI**:
| Library | From dependency | arm64-v8a | x86_64 | armeabi-v7a / x86 (32-bit, not gated) |
|---|---|---|---|---|
| `libsqlcipher.so` | `net.zetetic:sqlcipher-android:4.16.0` | 0x4000 OK | 0x4000 OK | 0x4000 OK |
| `libandroidx.graphics.path.so` | Compose (BOM `2026.06.00`) | 0x4000 OK | 0x4000 OK | 0x4000 OK |
| `libdatastore_shared_counter.so` | `androidx.datastore:1.2.1` | 0x4000 OK | 0x4000 OK | 0x4000 OK |
SQLCipher — the dependency called out in issue #17 — has shipped 16 KB-aligned binaries since
well before 4.16.0, and the pinned version is confirmed aligned above. AGP 9.2 also emits 16
KB-zip-aligned uncompressed libraries by default (AGP ≥ 8.5.1 behavior), and Play regenerates
delivery APKs from the AAB anyway. Re-verify after any bump of `sqlcipher`, `datastore`, or
`composeBom` in `gradle/libs.versions.toml`: Play Console → **App bundle explorer** shows a
16 KB compliance verdict per upload.
## 3. App Bundle (AAB) — PASS, signing is a human step
- `./gradlew :app:bundleRelease` succeeds and produces
`app/build/outputs/bundle/release/app-release.aab` (~10.4 MB, R8-minified). Verified
2026-07-01 with JDK 21.
- **Signing:** without `secrets.properties` the release build intentionally falls back to the
**debug** keystore (`app/build.gradle.kts:86` — installable locally, not publishable). For
Play the maintainer must create an upload keystore, set `RELEASE_STORE_FILE` /
`RELEASE_STORE_PASSWORD` / `RELEASE_KEY_ALIAS` / `RELEASE_KEY_PASSWORD` in
`secrets.properties`, rebuild, and enroll in **Play App Signing** on first upload (Play holds
the app signing key; the local key becomes the upload key).
- `versionCode 1` / `versionName "0.1.0"` (`app/build.gradle.kts:53`) — bump per release.
## 4. OAuth / CASA — no Google verification applies
Verified in source, 2026-07-01:
- **Gmail onboarding uses an app password over IMAP/SMTP, not OAuth.** The Gmail preset
(`domain/model/MailProvider.kt:37`) is plain `imap.gmail.com:993` / `smtp.gmail.com:587`
authenticating with a user-created app password; the only Google URL in the app is the
`myaccount.google.com/apppasswords` help link opened in the browser. There is **no Google
OAuth client, no Google sign-in flow, and no Gmail API scope anywhere in the code** — so the
Google restricted-scope verification and **CASA security assessment do not apply** to
LibreMail. (This is deliberate — issue #9; do not "fix" Gmail back to OAuth.)
- **Outlook OAuth is Microsoft-side only.** `auth/OutlookAuthManager.kt` uses AppAuth (PKCE,
public client) against `login.microsoftonline.com` with Microsoft Graph
(`Mail.Send`) and Exchange Online (`IMAP.AccessAsUser.All`, `SMTP.Send`) scopes. Verification
of that client is governed by **Microsoft's** app-registration/publisher rules in Azure —
nothing on the Google side. Google Play itself imposes no OAuth review; only the data-safety
and permissions declarations above cover it.
- README follow-up: tracked as issue **#20** (a fuller README pass); `README.md`'s privacy
section now links `PRIVACY.md`.
## 5. Console checklist (human steps, in order)
Everything below happens in Play Console and cannot be done from the repo. Drafted answers are
ready to paste.
1. **Developer account** — one-time registration + identity verification.
2. **Create app** — name *LibreMail*, default language, **App** (not game), **Free**.
Free-to-paid can never be toggled later; LibreMail is GPL and free.
3. **Store listing** (assets required):
- App icon **512×512 PNG** (≤1 MB); feature graphic **1024×500**; **2–8 phone screenshots**
(16:9 or 9:16, 320–3840 px; onboarding, inbox, reader, compose, settings are good
candidates); optional 7"/10" tablet screenshots.
- Short description (≤80 chars), draft:
> Open-source email for Outlook, Gmail, Yahoo, iCloud and any IMAP provider.
- Full description (≤4000 chars), draft:
> LibreMail is a free and open-source (GPL-3.0) email client with a friendly Material You
> design. Add Outlook/Hotmail (OAuth sign-in), Gmail, Yahoo, iCloud (app password), or any
> IMAP/SMTP provider; read, search, and manage your mail offline-first; compose with rich
> text, signatures, attachments, and contact autocomplete; get instant new-mail
> notifications via IMAP IDLE push — no tracking, no ads, no analytics, and your mail
> never touches our servers because we don't have any. Optional extras: encrypted local
> cache (SQLCipher), unified inbox for multiple accounts, and full mail-history backfill
> with a retention cap.
- Category **Communication**; contact email (maintainer's); privacy policy URL
`https://github.com/JMR-dev/LibreMail/blob/main/PRIVACY.md`.
4. **App content declarations:**
- **Privacy policy** — URL above.
- **Ads** — *No, my app does not contain ads* (no ad SDK; see dependency audit in
[`play-data-safety.md`](play-data-safety.md)).
- **App access** — reviewers need a mail account to exercise the app. Provide either
"All functionality is available without special access" plus a note that any IMAP account
works, or (safer) supply a disposable test account (e.g. a throwaway IMAP mailbox) under
*Special access instructions*. Do **not** hand over a personal account.
- **Content rating (IARC questionnaire)** — draft answers: email/communication app; category
**Utility / Communication**; violence/sex/language/drugs/gambling: **No** to all;
user interaction: **Yes** (users exchange email — expect an "Interactive elements: Users
Interact" notice); shares user-provided location: **No**; digital purchases: **No**.
Expected rating: **Everyone / PEGI 3** with the Users-Interact disclosure.
- **Target audience** — **13 and over** (requires an email account; not directed at
children — do not select under-13, which triggers Families policy).
- **News app** — No. **COVID-19 app** — No. **Government app** — No.
- **Financial features** — None. **Health apps** — Not a health app.
- **Data safety** — answers and evidence in [`play-data-safety.md`](play-data-safety.md).
- **Foreground service permissions** (`FOREGROUND_SERVICE_DATA_SYNC`) — declaration text and
demo-video script in [`play-permissions.md`](play-permissions.md).
- **Account deletion** — Play's deletion-URL requirement applies to apps that let users
*create an account with the developer*. LibreMail creates no such accounts (users connect
their own third-party mailboxes), so answer the "App access/account creation" question
with **no account creation** and the deletion section does not apply. In-app truth, if a
free-text answer is wanted:
> LibreMail has no user accounts of its own and stores data only on the device. Removing
> an account inside the app deletes its saved credentials and its locally cached
> messages, folders, and settings (`AccountRepositoryImpl.deleteAccount`); uninstalling
> the app removes all app data. The user's mailbox at their email provider is unaffected.
5. **Upload** the properly signed release AAB to **Internal testing** first; check **App bundle
explorer** (16 KB verdict) and the **pre-launch report** (automated crawl on real devices;
supply the test-account credentials so it can get past onboarding).
6. **Countries/regions**, pricing (Free), then promote Internal → Closed/Open testing →
Production. Note: new personal developer accounts must run a closed test (12 testers /
14 days) before production access.
## 6. Findings for the maintainer (repo-side, discovered during verification)
1. **"Push mail" is on by default, not opt-in.** `SettingsRepository.kt:41` defaults
`pushIdle = true`, so the dataSync foreground service starts as soon as the first account is
added. The manifest comment (`AndroidManifest.xml:88` — "opt-in via Advanced Settings") and
README wording say opt-in. Either flip the default to `false` or fix the comments; the Play
FGS declaration drafted here describes the **actual** behavior (default-on, user-visible
toggle, persistent notification), which is acceptable to declare but must stay truthful.
2. **README tech-stack table says min SDK 33**; the build uses `minSdk 29`
(`app/build.gradle.kts:51`). Fix with the #20 README pass.
3. **README advertises an "app lock" (biometric/device-credential)** that does not exist in the
code yet (no biometric API usage anywhere in `app/src/main`). `PRIVACY.md` deliberately does
not claim it; remove or de-scope the README claim until implemented (#20), and update
`PRIVACY.md` when it ships.
4. **Release signing falls back to the debug key** without `secrets.properties` — fine for CI,
but the Play upload must be built with the real upload keystore (section 3).
+110
View File
@@ -0,0 +1,110 @@
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
# Google Play Data safety form — mapping (issue #17)
Fill-in guide for Play Console → **App content → Data safety**. Every answer below is grounded
in this repository's code; re-verify against source if the data flows change. Companion docs:
[`PRIVACY.md`](../PRIVACY.md) (the policy to link in the form),
[`play-permissions.md`](play-permissions.md), [`play-compliance.md`](play-compliance.md).
## How Play defines "collection", and why LibreMail declares none
Play's definition ([Play Console Help — Provide information for Google Play's Data safety
section](https://support.google.com/googleplay/android-developer/answer/10787469)): *"Collect"
means transmitting data from your app off a user's device*, with exemptions that do **not** need
to be disclosed:
1. **On-device access/processing** — data "only processed locally on the user's device and not
sent off device".
2. **End-to-end encryption** — data unreadable by anyone other than sender and recipient.
3. **Ephemeral processing** — data held in memory and "retained for no longer than necessary to
service a specific request in real time".
LibreMail's data flows fall under exemptions 1 and 3:
- The developer **operates no servers and receives no user data**. There is no analytics,
crash-reporting, or ad SDK in the dependency tree (see audit below), and the only
developer-directed channel that exists in code — opt-in debug-report upload
(`app/src/main/kotlin/org/libremail/reporting/ReportUploadWorker.kt`) — is dead in shipped
builds because `DEBUG_REPORT_ENDPOINT` defaults to `""` (`app/build.gradle.kts`), which makes
the upload worker fail without transmitting.
- All other traffic is the app doing its job as the user's mail agent against **servers the
user chose** (their own IMAP/SMTP provider; Microsoft's OAuth/Graph endpoints for the Outlook
account type). Each transfer services a specific user request in real time (sign-in, sync,
send, server search); the app retains nothing off-device and the developer can never access
any of it.
The same reasoning is the established practice of comparable open-source mail clients on Play
that declare no collection. If a Play reviewer pushes back, use the conservative alternative at
the bottom of this page — it is also truthful.
## Form answers
| Form question | Answer |
|---|---|
| Does your app collect or share any of the required user data types? | **No** |
| Is all of the user data collected by your app encrypted in transit? | Not asked when "No" above; for the record: **yes**, all connections are TLS (`ImapClient.kt`, `SmtpSender.kt` set `ssl.checkserveridentity=true`; `MailSecurity.NONE` is not offered in the UI — `ManualSetupScreen.kt:211`) |
| Do you provide a way for users to request that their data is deleted? | Not asked when "No" above; see account-deletion notes in [`play-compliance.md`](play-compliance.md) |
| Privacy policy URL | `https://github.com/JMR-dev/LibreMail/blob/main/PRIVACY.md` |
Result shown on the store listing: **"No data collected"** / **"No data shared with third
parties"**.
## Category-by-category evidence
Every Play data-safety category, the truthful answer, and where the code proves it:
| Play category | Collected? | Shared? | Evidence in code |
|---|---|---|---|
| Personal info → Name | No | No | Account display name stored in local Room DB only (`data/local/entity/AccountEntity` via `AccountRepositoryImpl.kt`); appears off-device only inside mail the user sends |
| Personal info → Email address | No | No | The user's own address is their mail login, sent only to their chosen provider to authenticate/send (`ImapClient.kt`, `SmtpSender.kt`, `GraphSender.kt`) — user-initiated, real-time, never to the developer |
| Personal info → User IDs | No | No | No developer-side accounts or IDs exist; OAuth tokens go only between the device and `login.microsoftonline.com` (`auth/OutlookAuthManager.kt`) |
| Financial info / Health / Location | No | No | No such APIs or permissions anywhere in the merged manifest (see [`play-permissions.md`](play-permissions.md)) |
| Messages → Emails | No | No | Mail syncs from the user's server *to* the device (`MailSyncer`), is cached locally (Room, optional SQLCipher — `di/DatabaseModule.kt`), and is transmitted only when the user sends a message to their own SMTP/Graph endpoint |
| Photos and videos / Audio files / Files and docs | No | No | Attachments are chosen via the system document picker (`ComposeScreen.kt` `OpenMultipleDocuments`, no storage permission), stored under `cacheDir` (`MailRepositoryImpl.kt:361`), and leave the device only inside mail the user sends |
| Calendar | No | No | No calendar API usage |
| Contacts | **No** | No | `contacts/ContactsRepository.kt` queries `ContactsContract` **on-device** for ≤8 autocomplete matches; results are held in memory for the compose screen. Nothing is uploaded — Play's on-device exemption applies |
| App activity (interactions, search history, installed apps) | No | No | No analytics SDK; server search sends the query string to the *user's own* IMAP server as an IMAP `SEARCH` command (user-initiated, ephemeral) |
| Web browsing | No | No | The reader WebView has JavaScript disabled and network loads blocked unless the user enables remote images (`ui/reader/HtmlBody.kt:62,98`) — and even then requests go to hosts referenced by the email, not to the developer |
| App info and performance (crash logs, diagnostics) | **No** | No | Crash/debug reports are written to local app storage only (`reporting/ReportStore.kt` → `filesDir/debug_reports`); upload requires an explicit user tap **and** a configured endpoint, and the endpoint is empty in this repo (`ReportSubmitter.isEnabled` → false) |
| Device or other IDs | No | No | No advertising ID (no `AD_ID` permission in the merged manifest), no device-ID reads; debug reports include only `Build.MANUFACTURER`/`MODEL`/OS version, and stay on device (`reporting/DiagnosticsCollector.kt`) |
### Dependency audit (no ads / analytics / tracking SDKs)
The complete runtime dependency list (`app/build.gradle.kts` + `gradle/libs.versions.toml`) is:
AndroidX (core, lifecycle, activity, navigation, webkit, Compose BOM, Room, DataStore,
WorkManager, Hilt-androidx), Dagger Hilt, kotlinx-coroutines, Eclipse Angus Mail (IMAP/SMTP),
AppAuth-Android (OAuth), and Zetetic SQLCipher. There is **no** Google Play Services, Firebase,
ad, analytics, or crash-reporting dependency, and the merged release manifest contains no
`com.google.android.gms.permission.AD_ID` permission (verified in
`app/build/intermediates/merged_manifests/release/processReleaseManifest/AndroidManifest.xml`).
## Security-practices section of the form
- **Encrypted in transit:** yes — TLS everywhere, hostname verification pinned on
(`mail.<proto>.ssl.checkserveridentity=true` in `ImapClient.kt:480` / `SmtpSender.kt:50`).
- **Encryption at rest (optional extra credit, not a form field):** credentials are always
encrypted with an Android Keystore key (`data/security/KeystoreCrypto.kt`,
`CredentialStore.kt`); the mail cache can be SQLCipher-encrypted with a Keystore-sealed random
key (`data/security/DatabaseKeyStore.kt`, opt-in, default off —
`SettingsRepository.kt:44`).
- **Independent security review badge:** not requested (optional program).
## If anything changes, this form must change
| Future change | Data-safety impact |
|---|---|
| Configuring a real `DEBUG_REPORT_ENDPOINT` (issue #34) | Declare **App info and performance → Crash logs / Diagnostics**: collected, optional (user-initiated), not shared, encrypted in transit, user can delete (reports are deletable pre-submit) |
| Any opt-in telemetry from issues #10/#11 | Declare the specific types as collected + optional; backlog decision requires it stay strictly opt-in (F-Droid constraint) |
| Any new SDK with network access | Re-run this audit; SDKs count toward the form ("data transmitted by libraries/SDKs") |
## Conservative alternative declaration (only if Google rejects "no collection")
Declare the following, all with *Collected: yes · Optional: no · Shared: no · Processed
ephemerally: yes · Purpose: App functionality · Encrypted in transit: yes · Deletion: user can
delete data in-app (remove account)*:
- Personal info → Email address (account sign-in)
- Messages → Emails (sending/syncing the user's own mail with their provider)
Contacts, crash logs, and diagnostics remain **not collected** under any reading — they
demonstrably never leave the device in this codebase.
+112
View File
@@ -0,0 +1,112 @@
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
# Permissions justification — merged manifest audit (issue #17)
Every permission in the **merged release manifest** (source of truth:
`app/build/intermediates/merged_manifests/release/processReleaseManifest/AndroidManifest.xml`
after `./gradlew :app:bundleRelease`; attribution from
`app/build/outputs/logs/manifest-merger-release-report.txt`), why it exists, where it is used,
and the text to paste into Play Console where a declaration is required.
## Complete merged-manifest permission list
| Permission | Declared by | Runtime prompt? | Purpose |
|---|---|---|---|
| `INTERNET` | app manifest | No | IMAP/SMTP/OAuth/Graph connections to the user's mail provider |
| `ACCESS_NETWORK_STATE` | app manifest | No | Connectivity checks so sync/WorkManager runs only when online |
| `READ_CONTACTS` | app manifest | **Yes** | On-device recipient autocomplete in the compose screen |
| `POST_NOTIFICATIONS` | app manifest | **Yes** (API 33+) | New-mail notifications + mandatory foreground-service status notification |
| `FOREGROUND_SERVICE` | app manifest | No | Prerequisite for running any foreground service (API 28+) |
| `FOREGROUND_SERVICE_DATA_SYNC` | app manifest | No | Type-specific permission for the IMAP IDLE push service (API 34+) |
| `WAKE_LOCK` | `androidx.work:work-runtime:2.11.2` | No | WorkManager keeps the CPU awake while a scheduled job (mail sync, outbox send) runs |
| `RECEIVE_BOOT_COMPLETED` | `androidx.work:work-runtime:2.11.2` | No | WorkManager reschedules pending jobs (periodic sync, queued outbox mail) after reboot |
| `org.libremail.app.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION` | `androidx.core:core:1.17.0` | No | Auto-generated app-signature permission guarding non-exported runtime receivers; not user-facing |
Nothing else. Notably **absent** (worth stating in any review exchange):
- **No `AD_ID`** — no ads or analytics SDKs at all.
- **No storage/media permissions** — attachments use the Storage Access Framework
(`OpenMultipleDocuments` in `ui/compose/ComposeScreen.kt:88`) and a `FileProvider` for viewing
(`AndroidManifest.xml:95`).
- **No `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`** — deliberately avoided because Play restricts
it; the app deep-links to the system app-details screen instead
(`push/BatteryOptimizationManager.kt`, comment cites this issue).
- No location, camera, microphone, SMS, call-log, accessibility, or `QUERY_ALL_PACKAGES`.
## `READ_CONTACTS` (Play "sensitive" permission — scrutinized, no declaration form)
- **Feature:** recipient autocomplete while composing. `contacts/ContactsRepository.kt` queries
`ContactsContract.CommonDataKinds.Email` for at most 8 name/email matches of the typed text.
- **Data handling:** query and results are entirely **on-device** (results live in memory for
the suggestion dropdown). Nothing from the contacts provider is stored, logged, or
transmitted; an address reaches the network only if the user puts it on an email they send.
- **Request flow:** first composition of the compose screen (`ui/compose/ComposeScreen.kt:101`);
denial is handled gracefully — `ContactsRepository.search` returns empty and composing works
normally (manual address entry).
- **Play-Console justification text (if asked in review):**
> LibreMail is an email client. READ_CONTACTS powers recipient autocomplete on the compose
> screen only: the app queries the on-device contacts provider for names/email addresses
> matching what the user typed and shows up to 8 suggestions. Contact data is processed
> entirely on the device — it is never uploaded, stored outside the suggestion list, or shared.
> The permission is requested in context (first open of the compose screen) and the feature
> degrades gracefully if denied.
## `POST_NOTIFICATIONS`
- **Features:** (1) per-account new-mail notifications, generated on-device from synced mail —
`notifications/MailNotifier.kt` (no push/cloud-messaging service; lock-screen content
redacted via `VISIBILITY_PRIVATE`); (2) the persistent low-importance status notification
Android requires while the IMAP IDLE foreground service runs (`push/IdleService.kt:120`).
- **Request flow:** once at first launch, API 33+ only (`MainActivity.kt`
`NotificationPermissionEffect`). If denied, `MailNotifier.notifyNewMail` no-ops (permission
re-checked before every post, `MailNotifier.kt:134`); mail sync itself is unaffected.
- **Play-Console justification text (if asked):**
> Notifies the user of newly received email (per-account channels, generated on the device
> from the user's own mailbox — no push service) and shows the persistent status notification
> Android requires for the optional foreground IMAP IDLE connection. Requested once at first
> launch; all app functions except notifications work if declined.
## `FOREGROUND_SERVICE_DATA_SYNC` (requires the Play Console FGS declaration)
Play Console → App content → **Foreground service permissions** asks for the type's use case
and a demo video. Facts to declare, all verifiable in `push/IdleService.kt`:
- **What runs:** one foreground service (`.push.IdleService`, manifest
`foregroundServiceType="dataSync"`, `AndroidManifest.xml:89`) holding a long-lived IMAP IDLE
(RFC 2177) connection per configured account so the user's own mail server can push new mail
instantly. On server activity it triggers a normal sync into the local cache and a new-mail
notification.
- **Why a foreground service:** IMAP IDLE requires a continuously open TCP connection that
survives while the app is backgrounded; it cannot be modeled as deferrable work. WorkManager
**is** used for everything deferrable (periodic sync, outbox sending) — the service exists
only for the always-connected push case. There is no push-notification alternative (FCM)
because plain IMAP servers cannot address one, and the app deliberately uses no Google cloud
services.
- **User control / lifecycle:** starts only when at least one account exists **and** the "push
mail" setting is enabled (`LibreMailApplication.kt:70`); the setting is a visible toggle in
Settings (`ui/settings/SettingsScreen.kt:147`); the service stops reactively when the toggle
turns off or the last account is removed, and shows a persistent low-importance status
notification while running (`IdleService.startAsForeground`).
- **Declaration text to paste:**
> LibreMail is an email client. The dataSync foreground service maintains a long-lived IMAP
> IDLE (RFC 2177) connection to the user's own mail server so new mail arrives instantly.
> IMAP has no out-of-band push channel (such as FCM), so real-time delivery requires keeping
> this user-visible connection open; all deferrable transfers (periodic sync, sending queued
> mail) already use WorkManager instead. The service runs only while the user has an account
> configured and the "push mail" setting enabled, displays a persistent status notification,
> and stops immediately when the user disables the setting or removes their last account.
- **Demo video (human step):** screen-record: Settings → toggle "push mail" on → the status
notification appears → send the account a mail from elsewhere → the new-mail notification
arrives with the app backgrounded → toggle off → status notification disappears.
- **Note:** the app targets SDK 37, so the API-34 requirement to declare a type for every FGS
is in force; `FOREGROUND_SERVICE` plus the typed permission are both declared and the service
calls `ServiceCompat.startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC)`
(`IdleService.kt:136`).
## Library-injected permissions (`WAKE_LOCK`, `RECEIVE_BOOT_COMPLETED`)
Injected by `androidx.work:work-runtime:2.11.2` for its own machinery: holding a partial wake
lock while an enqueued job executes, and re-registering scheduled jobs after a reboot. LibreMail
uses WorkManager for periodic mail sync (`data/sync/` workers), reliable outbox sending
(`SendWorker.kt`), and the opt-in debug-report upload (`ReportUploadWorker.kt`, dormant —
endpoint unconfigured). Neither permission needs a Play declaration; keep this attribution handy
for review questions.