Loading app/core/src/main/java/com/fsck/k9/helper/AddressFormatter.kt 0 → 100644 +74 −0 Original line number Diff line number Diff line package com.fsck.k9.helper import android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE import android.text.SpannableString import android.text.style.ForegroundColorSpan import com.fsck.k9.Account import com.fsck.k9.Identity import com.fsck.k9.mail.Address /** * Get the display name for an email address. */ interface AddressFormatter { fun getDisplayName(address: Address): CharSequence } class RealAddressFormatter( private val contactNameProvider: ContactNameProvider, private val account: Account, private val showCorrespondentNames: Boolean, private val showContactNames: Boolean, private val contactNameColor: Int?, private val meText: String ) : AddressFormatter { override fun getDisplayName(address: Address): CharSequence { val identity = account.findIdentity(address) if (identity != null) { return getIdentityName(identity) } return if (!showCorrespondentNames) { address.address } else if (showContactNames) { getContactName(address) } else { buildDisplayName(address) } } private fun getIdentityName(identity: Identity): String { return if (account.identities.size == 1) { meText } else { identity.description ?: identity.name ?: identity.email ?: meText } } private fun getContactName(address: Address): CharSequence { val contactName = contactNameProvider.getNameForAddress(address.address) ?: return buildDisplayName(address) return if (contactNameColor != null) { SpannableString(contactName).apply { setSpan(ForegroundColorSpan(contactNameColor), 0, contactName.length, SPAN_EXCLUSIVE_EXCLUSIVE) } } else { contactName } } private fun buildDisplayName(address: Address): CharSequence { return address.personal?.takeIf { it.isNotBlank() && !it.equals(meText, ignoreCase = true) && !isSpoofAddress(it) } ?: address.address } private fun isSpoofAddress(displayName: String): Boolean { val atIndex = displayName.indexOf('@') return if (atIndex > 0) { displayName[atIndex - 1] != '(' } else { false } } } app/core/src/main/java/com/fsck/k9/helper/ContactNameProvider.kt 0 → 100644 +11 −0 Original line number Diff line number Diff line package com.fsck.k9.helper interface ContactNameProvider { fun getNameForAddress(address: String): String? } class RealContactNameProvider(private val contacts: Contacts) : ContactNameProvider { override fun getNameForAddress(address: String): String? { return contacts.getNameForAddress(address) } } app/core/src/test/java/com/fsck/k9/helper/RealAddressFormatterTest.kt 0 → 100644 +206 −0 Original line number Diff line number Diff line package com.fsck.k9.helper import android.graphics.Color import android.text.Spannable import android.text.style.ForegroundColorSpan import androidx.core.text.getSpans import com.fsck.k9.Account import com.fsck.k9.Identity import com.fsck.k9.RobolectricTest import com.fsck.k9.mail.Address import com.google.common.truth.Truth.assertThat import org.junit.Test private const val IDENTITY_ADDRESS = "me@domain.example" private const val ME_TEXT = "me" class RealAddressFormatterTest : RobolectricTest() { private val contactNameProvider = object : ContactNameProvider { override fun getNameForAddress(address: String): String? { return when (address) { "user1@domain.example" -> "Contact One" "spoof@domain.example" -> "contact@important.example" else -> null } } } private val account = Account("uuid").apply { identities += Identity(email = IDENTITY_ADDRESS) } @Test fun `single identity`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant")) assertThat(displayName).isEqualTo(ME_TEXT) } @Test fun `multiple identities`() { val account = Account("uuid").apply { identities += Identity(description = "My identity", email = IDENTITY_ADDRESS) identities += Identity(email = "another.one@domain.example") } val addressFormatter = createAddressFormatter(account) val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant")) assertThat(displayName).isEqualTo("My identity") } @Test fun `identity without a description`() { val account = Account("uuid").apply { identities += Identity(name = "My name", email = IDENTITY_ADDRESS) identities += Identity(email = "another.one@domain.example") } val addressFormatter = createAddressFormatter(account) val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant")) assertThat(displayName).isEqualTo("My name") } @Test fun `identity without a description and name`() { val account = Account("uuid").apply { identities += Identity(email = IDENTITY_ADDRESS) identities += Identity(email = "another.one@domain.example") } val addressFormatter = createAddressFormatter(account) val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant")) assertThat(displayName).isEqualTo(IDENTITY_ADDRESS) } @Test fun `email address without display name`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("alice@domain.example")) assertThat(displayName).isEqualTo("alice@domain.example") } @Test fun `email address with display name`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("alice@domain.example", "Alice")) assertThat(displayName).isEqualTo("Alice") } @Test fun `don't look up contact when showContactNames = false`() { val addressFormatter = createAddressFormatter(showContactNames = false) val displayName = addressFormatter.getDisplayName(Address("user1@domain.example", "User 1")) assertThat(displayName).isEqualTo("User 1") } @Test fun `contact lookup`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("user1@domain.example")) assertThat(displayName).isEqualTo("Contact One") } @Test fun `contact lookup despite display name`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("user1@domain.example", "User 1")) assertThat(displayName).isEqualTo("Contact One") } @Test fun `colored contact name`() { val addressFormatter = createAddressFormatter(contactNameColor = Color.RED) val displayName = addressFormatter.getDisplayName(Address("user1@domain.example")) assertThat(displayName.toString()).isEqualTo("Contact One") assertThat(displayName).isInstanceOf(Spannable::class.java) val spans = (displayName as Spannable).getSpans<ForegroundColorSpan>(0, displayName.length) assertThat(spans.map { it.foregroundColor }).containsExactly(Color.RED) } @Test fun `email address with display name but not showing correspondent names`() { val addressFormatter = createAddressFormatter(showCorrespondentNames = false) val displayName = addressFormatter.getDisplayName(Address("alice@domain.example", "Alice")) assertThat(displayName).isEqualTo("alice@domain.example") } @Test fun `do not show display name that looks like an email address`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("mallory@domain.example", "potus@whitehouse.gov")) assertThat(displayName).isEqualTo("mallory@domain.example") } @Test fun `do show display name that contains an @ preceded by an opening parenthesis`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("gitlab@gitlab.example", "username (@username)")) assertThat(displayName).isEqualTo("username (@username)") } @Test fun `do show display name that starts with an @`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("address@domain.example", "@username")) assertThat(displayName).isEqualTo("@username") } @Test fun `spoof prevention doesn't apply to contact names`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("spoof@domain.example", "contact@important.example")) assertThat(displayName).isEqualTo("contact@important.example") } @Test fun `display name matches me text`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("someone_named_me@domain.example", "ME")) assertThat(displayName).isEqualTo("someone_named_me@domain.example") } private fun createAddressFormatter( account: Account = this.account, showCorrespondentNames: Boolean = true, showContactNames: Boolean = true, contactNameColor: Int? = null ): RealAddressFormatter { return RealAddressFormatter( contactNameProvider = contactNameProvider, account = account, showCorrespondentNames = showCorrespondentNames, showContactNames = showContactNames, contactNameColor = contactNameColor, meText = ME_TEXT ) } } app/k9mail/proguard-rules.pro +4 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,10 @@ -dontnote com.fsck.k9.ui.messageview.** -dontnote com.fsck.k9.view.** -assumevalues class * extends android.view.View { boolean isInEditMode() return false; } -keep public class org.openintents.openpgp.** -keepclassmembers class * extends androidx.appcompat.widget.SearchView { Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/DisplayRecipientsExtractor.kt 0 → 100644 +59 −0 Original line number Diff line number Diff line package com.fsck.k9.ui.messageview import com.fsck.k9.Account import com.fsck.k9.helper.AddressFormatter import com.fsck.k9.mail.Address import com.fsck.k9.mail.Message /** * Extract recipient names from a message to display them in the message view. * * This class extracts up to [maxNumberOfDisplayRecipients] recipients from the message and converts them to their * display name using an [AddressFormatter]. */ internal class DisplayRecipientsExtractor( private val addressFormatter: AddressFormatter, private val maxNumberOfDisplayRecipients: Int ) { fun extractDisplayRecipients(message: Message, account: Account): DisplayRecipients { val toRecipients = message.getRecipients(Message.RecipientType.TO) val ccRecipients = message.getRecipients(Message.RecipientType.CC) val bccRecipients = message.getRecipients(Message.RecipientType.BCC) val numberOfRecipients = toRecipients.size + ccRecipients.size + bccRecipients.size val identity = sequenceOf(toRecipients, ccRecipients, bccRecipients) .flatMap { addressArray -> addressArray.asSequence() } .mapNotNull { address -> account.findIdentity(address) } .firstOrNull() val identityEmail = identity?.email val maxAdditionalRecipients = if (identity != null) { maxNumberOfDisplayRecipients - 1 } else { maxNumberOfDisplayRecipients } val recipientNames = sequenceOf(toRecipients, ccRecipients, bccRecipients) .flatMap { addressArray -> addressArray.asSequence() } .filter { address -> address.address != identityEmail } .map { address -> addressFormatter.getDisplayName(address) } .take(maxAdditionalRecipients) .toList() return if (identity != null) { val identityAddress = Address(identity.email) val meName = addressFormatter.getDisplayName(identityAddress) val recipients = listOf(meName) + recipientNames DisplayRecipients(recipients, numberOfRecipients) } else { DisplayRecipients(recipientNames, numberOfRecipients) } } } internal data class DisplayRecipients( val recipientNames: List<CharSequence>, val numberOfRecipients: Int ) Loading
app/core/src/main/java/com/fsck/k9/helper/AddressFormatter.kt 0 → 100644 +74 −0 Original line number Diff line number Diff line package com.fsck.k9.helper import android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE import android.text.SpannableString import android.text.style.ForegroundColorSpan import com.fsck.k9.Account import com.fsck.k9.Identity import com.fsck.k9.mail.Address /** * Get the display name for an email address. */ interface AddressFormatter { fun getDisplayName(address: Address): CharSequence } class RealAddressFormatter( private val contactNameProvider: ContactNameProvider, private val account: Account, private val showCorrespondentNames: Boolean, private val showContactNames: Boolean, private val contactNameColor: Int?, private val meText: String ) : AddressFormatter { override fun getDisplayName(address: Address): CharSequence { val identity = account.findIdentity(address) if (identity != null) { return getIdentityName(identity) } return if (!showCorrespondentNames) { address.address } else if (showContactNames) { getContactName(address) } else { buildDisplayName(address) } } private fun getIdentityName(identity: Identity): String { return if (account.identities.size == 1) { meText } else { identity.description ?: identity.name ?: identity.email ?: meText } } private fun getContactName(address: Address): CharSequence { val contactName = contactNameProvider.getNameForAddress(address.address) ?: return buildDisplayName(address) return if (contactNameColor != null) { SpannableString(contactName).apply { setSpan(ForegroundColorSpan(contactNameColor), 0, contactName.length, SPAN_EXCLUSIVE_EXCLUSIVE) } } else { contactName } } private fun buildDisplayName(address: Address): CharSequence { return address.personal?.takeIf { it.isNotBlank() && !it.equals(meText, ignoreCase = true) && !isSpoofAddress(it) } ?: address.address } private fun isSpoofAddress(displayName: String): Boolean { val atIndex = displayName.indexOf('@') return if (atIndex > 0) { displayName[atIndex - 1] != '(' } else { false } } }
app/core/src/main/java/com/fsck/k9/helper/ContactNameProvider.kt 0 → 100644 +11 −0 Original line number Diff line number Diff line package com.fsck.k9.helper interface ContactNameProvider { fun getNameForAddress(address: String): String? } class RealContactNameProvider(private val contacts: Contacts) : ContactNameProvider { override fun getNameForAddress(address: String): String? { return contacts.getNameForAddress(address) } }
app/core/src/test/java/com/fsck/k9/helper/RealAddressFormatterTest.kt 0 → 100644 +206 −0 Original line number Diff line number Diff line package com.fsck.k9.helper import android.graphics.Color import android.text.Spannable import android.text.style.ForegroundColorSpan import androidx.core.text.getSpans import com.fsck.k9.Account import com.fsck.k9.Identity import com.fsck.k9.RobolectricTest import com.fsck.k9.mail.Address import com.google.common.truth.Truth.assertThat import org.junit.Test private const val IDENTITY_ADDRESS = "me@domain.example" private const val ME_TEXT = "me" class RealAddressFormatterTest : RobolectricTest() { private val contactNameProvider = object : ContactNameProvider { override fun getNameForAddress(address: String): String? { return when (address) { "user1@domain.example" -> "Contact One" "spoof@domain.example" -> "contact@important.example" else -> null } } } private val account = Account("uuid").apply { identities += Identity(email = IDENTITY_ADDRESS) } @Test fun `single identity`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant")) assertThat(displayName).isEqualTo(ME_TEXT) } @Test fun `multiple identities`() { val account = Account("uuid").apply { identities += Identity(description = "My identity", email = IDENTITY_ADDRESS) identities += Identity(email = "another.one@domain.example") } val addressFormatter = createAddressFormatter(account) val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant")) assertThat(displayName).isEqualTo("My identity") } @Test fun `identity without a description`() { val account = Account("uuid").apply { identities += Identity(name = "My name", email = IDENTITY_ADDRESS) identities += Identity(email = "another.one@domain.example") } val addressFormatter = createAddressFormatter(account) val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant")) assertThat(displayName).isEqualTo("My name") } @Test fun `identity without a description and name`() { val account = Account("uuid").apply { identities += Identity(email = IDENTITY_ADDRESS) identities += Identity(email = "another.one@domain.example") } val addressFormatter = createAddressFormatter(account) val displayName = addressFormatter.getDisplayName(Address(IDENTITY_ADDRESS, "irrelevant")) assertThat(displayName).isEqualTo(IDENTITY_ADDRESS) } @Test fun `email address without display name`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("alice@domain.example")) assertThat(displayName).isEqualTo("alice@domain.example") } @Test fun `email address with display name`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("alice@domain.example", "Alice")) assertThat(displayName).isEqualTo("Alice") } @Test fun `don't look up contact when showContactNames = false`() { val addressFormatter = createAddressFormatter(showContactNames = false) val displayName = addressFormatter.getDisplayName(Address("user1@domain.example", "User 1")) assertThat(displayName).isEqualTo("User 1") } @Test fun `contact lookup`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("user1@domain.example")) assertThat(displayName).isEqualTo("Contact One") } @Test fun `contact lookup despite display name`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("user1@domain.example", "User 1")) assertThat(displayName).isEqualTo("Contact One") } @Test fun `colored contact name`() { val addressFormatter = createAddressFormatter(contactNameColor = Color.RED) val displayName = addressFormatter.getDisplayName(Address("user1@domain.example")) assertThat(displayName.toString()).isEqualTo("Contact One") assertThat(displayName).isInstanceOf(Spannable::class.java) val spans = (displayName as Spannable).getSpans<ForegroundColorSpan>(0, displayName.length) assertThat(spans.map { it.foregroundColor }).containsExactly(Color.RED) } @Test fun `email address with display name but not showing correspondent names`() { val addressFormatter = createAddressFormatter(showCorrespondentNames = false) val displayName = addressFormatter.getDisplayName(Address("alice@domain.example", "Alice")) assertThat(displayName).isEqualTo("alice@domain.example") } @Test fun `do not show display name that looks like an email address`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("mallory@domain.example", "potus@whitehouse.gov")) assertThat(displayName).isEqualTo("mallory@domain.example") } @Test fun `do show display name that contains an @ preceded by an opening parenthesis`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("gitlab@gitlab.example", "username (@username)")) assertThat(displayName).isEqualTo("username (@username)") } @Test fun `do show display name that starts with an @`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("address@domain.example", "@username")) assertThat(displayName).isEqualTo("@username") } @Test fun `spoof prevention doesn't apply to contact names`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("spoof@domain.example", "contact@important.example")) assertThat(displayName).isEqualTo("contact@important.example") } @Test fun `display name matches me text`() { val addressFormatter = createAddressFormatter() val displayName = addressFormatter.getDisplayName(Address("someone_named_me@domain.example", "ME")) assertThat(displayName).isEqualTo("someone_named_me@domain.example") } private fun createAddressFormatter( account: Account = this.account, showCorrespondentNames: Boolean = true, showContactNames: Boolean = true, contactNameColor: Int? = null ): RealAddressFormatter { return RealAddressFormatter( contactNameProvider = contactNameProvider, account = account, showCorrespondentNames = showCorrespondentNames, showContactNames = showContactNames, contactNameColor = contactNameColor, meText = ME_TEXT ) } }
app/k9mail/proguard-rules.pro +4 −0 Original line number Diff line number Diff line Loading @@ -29,6 +29,10 @@ -dontnote com.fsck.k9.ui.messageview.** -dontnote com.fsck.k9.view.** -assumevalues class * extends android.view.View { boolean isInEditMode() return false; } -keep public class org.openintents.openpgp.** -keepclassmembers class * extends androidx.appcompat.widget.SearchView { Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/DisplayRecipientsExtractor.kt 0 → 100644 +59 −0 Original line number Diff line number Diff line package com.fsck.k9.ui.messageview import com.fsck.k9.Account import com.fsck.k9.helper.AddressFormatter import com.fsck.k9.mail.Address import com.fsck.k9.mail.Message /** * Extract recipient names from a message to display them in the message view. * * This class extracts up to [maxNumberOfDisplayRecipients] recipients from the message and converts them to their * display name using an [AddressFormatter]. */ internal class DisplayRecipientsExtractor( private val addressFormatter: AddressFormatter, private val maxNumberOfDisplayRecipients: Int ) { fun extractDisplayRecipients(message: Message, account: Account): DisplayRecipients { val toRecipients = message.getRecipients(Message.RecipientType.TO) val ccRecipients = message.getRecipients(Message.RecipientType.CC) val bccRecipients = message.getRecipients(Message.RecipientType.BCC) val numberOfRecipients = toRecipients.size + ccRecipients.size + bccRecipients.size val identity = sequenceOf(toRecipients, ccRecipients, bccRecipients) .flatMap { addressArray -> addressArray.asSequence() } .mapNotNull { address -> account.findIdentity(address) } .firstOrNull() val identityEmail = identity?.email val maxAdditionalRecipients = if (identity != null) { maxNumberOfDisplayRecipients - 1 } else { maxNumberOfDisplayRecipients } val recipientNames = sequenceOf(toRecipients, ccRecipients, bccRecipients) .flatMap { addressArray -> addressArray.asSequence() } .filter { address -> address.address != identityEmail } .map { address -> addressFormatter.getDisplayName(address) } .take(maxAdditionalRecipients) .toList() return if (identity != null) { val identityAddress = Address(identity.email) val meName = addressFormatter.getDisplayName(identityAddress) val recipients = listOf(meName) + recipientNames DisplayRecipients(recipients, numberOfRecipients) } else { DisplayRecipients(recipientNames, numberOfRecipients) } } } internal data class DisplayRecipients( val recipientNames: List<CharSequence>, val numberOfRecipients: Int )