Files
LibreMail/docs/play-permissions.md
T
JMR-devandClaude Fable 5 3f7024d05d 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>
2026-07-01 21:52:40 -05:00

113 lines
8.0 KiB
Markdown

<!-- 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.