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

8.0 KiB

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.