fix: open the composer for mailto links sent with SENDTO

Most apps start an email with `ACTION_SENDTO` and a `mailto:` URI. The
manifest offered the app for it, but React Native's Linking only
forwards VIEW intents, so the app opened on the inbox instead of the
composer.

MainActivity now rewrites a `SENDTO mailto:` intent into the equivalent
`VIEW mailto:` link before React Native sees it, on a cold start and
when the running app receives it. The EXTRA_EMAIL, EXTRA_CC, EXTRA_BCC,
EXTRA_SUBJECT and EXTRA_TEXT extras are folded into the link's query.
Bcc recipients of a mailto link now reach the composer too; they were
dropped for VIEW links as well.
This commit is contained in:
Linus Rath
2026-09-24 18:43:43 +02:00
parent cd4ce9d08b
commit 06e89422bd
4 changed files with 72 additions and 2 deletions
@@ -17,12 +17,14 @@ class MainActivity : ReactActivity() {
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
setTheme(R.style.AppTheme);
ShareIntentStore.rewriteSendToAsView(intent)
super.onCreate(null)
NotificationTapStore.captureFromIntent(intent)
ShareIntentStore.captureFromIntent(intent, contentResolver)
}
override fun onNewIntent(intent: Intent) {
ShareIntentStore.rewriteSendToAsView(intent)
super.onNewIntent(intent)
setIntent(intent)
ShareIntentStore.captureFromIntent(intent, contentResolver)?.let { share ->
@@ -12,7 +12,8 @@ import com.facebook.react.bridge.WritableMap
// Process-wide holder for the most recent ACTION_SEND / ACTION_SEND_MULTIPLE
// intent (the system share sheet). MainActivity fills this on onCreate /
// onNewIntent; JS drains it via BulwarkFcm.getInitialShare or reacts to the
// live "app:share" event and opens the composer.
// live "app:share" event and opens the composer. `SENDTO mailto:` intents are
// rewritten into `VIEW mailto:` links instead (see rewriteSendToAsView).
object ShareIntentStore {
@Volatile
private var pending: SharePayload? = null
@@ -56,6 +57,40 @@ object ShareIntentStore {
return payload
}
// `SENDTO mailto:` is how most apps start an email, but React Native's
// Linking only forwards VIEW intents, so the app opened on the inbox.
// Turn it into the equivalent `VIEW mailto:` link, folding the recipient,
// subject and text extras into the URI, so it reaches JS like any other
// mailto link: Linking.getInitialURL on a cold start, the "url" event
// while running. Must run before the intent is handed to React Native.
fun rewriteSendToAsView(intent: Intent?) {
val action = intent?.action ?: return
if (action != Intent.ACTION_SENDTO) return
val data = intent.data ?: return
if (!"mailto".equals(data.scheme, ignoreCase = true)) return
val params = mutableListOf<String>()
addressExtra(intent, Intent.EXTRA_EMAIL).forEach { params.add("to=${Uri.encode(it)}") }
addressExtra(intent, Intent.EXTRA_CC).forEach { params.add("cc=${Uri.encode(it)}") }
addressExtra(intent, Intent.EXTRA_BCC).forEach { params.add("bcc=${Uri.encode(it)}") }
intent.getCharSequenceExtra(Intent.EXTRA_SUBJECT)?.let {
params.add("subject=${Uri.encode(it.toString().take(512))}")
}
intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.let {
params.add("body=${Uri.encode(it.toString().take(MAX_TEXT))}")
}
val link = data.toString()
val separator = if (link.contains('?')) "&" else "?"
intent.action = Intent.ACTION_VIEW
intent.data = if (params.isEmpty()) data else Uri.parse(link + separator + params.joinToString("&"))
}
// EXTRA_EMAIL/CC/BCC are documented as String[], but some apps put a
// single String.
private fun addressExtra(intent: Intent, key: String): List<String> =
intent.getStringArrayExtra(key)?.filterNotNull() ?: listOfNotNull(intent.getStringExtra(key))
private class SharedFile(val name: String, val mimeType: String, val size: Double)
// Ask the provider for the file's real name, type and size: a MediaStore