Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 6c1388f6 authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #6507 from thundernest/display_recipient_names

Display recipient names
parents e8e1b3c0 c2eb4ff0
Loading
Loading
Loading
Loading
+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
        }
    }
}
+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)
    }
}
+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
        )
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -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 {
+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