Add email value type

This commit is contained in:
Wolf Montwé 2023-03-03 13:05:39 +01:00
parent 7a9c373f92
commit 5e09e067b0
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
11 changed files with 84 additions and 51 deletions

View file

@ -1,11 +1,13 @@
package com.fsck.k9.helper
import app.k9mail.core.common.mail.EmailAddress
interface ContactNameProvider {
fun getNameForAddress(address: String): String?
}
class RealContactNameProvider(private val contacts: Contacts) : ContactNameProvider {
override fun getNameForAddress(address: String): String? {
return contacts.getNameForAddress(address)
return contacts.getNameFor(EmailAddress(address))
}
}

View file

@ -10,6 +10,7 @@ import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds
import androidx.core.content.ContextCompat
import app.k9mail.core.android.common.database.EmptyCursor
import app.k9mail.core.common.mail.EmailAddress
import com.fsck.k9.mail.Address
import timber.log.Timber
@ -28,9 +29,9 @@ open class Contacts(
* @return <tt>true</tt>, if the email address belongs to a contact.
* <tt>false</tt>, otherwise.
*/
fun isInContacts(emailAddress: String): Boolean {
fun isInContacts(emailAddress: EmailAddress): Boolean {
var result = false
val cursor = getContactByAddress(emailAddress)
val cursor = getContactFor(emailAddress)
if (cursor != null) {
if (cursor.count > 0) {
result = true
@ -41,26 +42,17 @@ open class Contacts(
}
/**
* Check whether one of the provided addresses belongs to one of the contacts.
* Check whether one of the provided email addresses belongs to one of the contacts.
*
* @param addresses The addresses to search in contacts
* @return <tt>true</tt>, if one address belongs to a contact.
* @param emailAddresses The email addresses to search in contacts
* @return <tt>true</tt>, if one of the email addresses belongs to a contact.
* <tt>false</tt>, otherwise.
*/
fun isAnyInContacts(addresses: Array<Address>?): Boolean {
if (addresses == null) {
return false
}
for (addr in addresses) {
if (isInContacts(addr.address)) {
return true
}
}
return false
}
fun isAnyInContacts(emailAddresses: List<EmailAddress>): Boolean =
emailAddresses.any { emailAddress -> isInContacts(emailAddress) }
fun getContactUri(emailAddress: String): Uri? {
val cursor = getContactByAddress(emailAddress) ?: return null
fun getContactUri(emailAddress: EmailAddress): Uri? {
val cursor = getContactFor(emailAddress) ?: return null
cursor.use {
if (!cursor.moveToFirst()) {
return null
@ -74,17 +66,15 @@ open class Contacts(
/**
* Get the name of the contact an email address belongs to.
*
* @param address The email address to search for.
* @param emailAddress The email address to search for.
* @return The name of the contact the email address belongs to. Or
* <tt>null</tt> if there's no matching contact.
*/
open fun getNameForAddress(address: String?): String? {
if (address == null) {
return null
} else if (nameCache.containsKey(address)) {
return nameCache[address]
open fun getNameFor(emailAddress: EmailAddress): String? {
if (nameCache.containsKey(emailAddress)) {
return nameCache[emailAddress]
}
val cursor = getContactByAddress(address)
val cursor = getContactFor(emailAddress)
var name: String? = null
if (cursor != null) {
if (cursor.count > 0) {
@ -93,7 +83,7 @@ open class Contacts(
}
cursor.close()
}
nameCache[address] = name
nameCache[emailAddress] = name
return name
}
@ -108,16 +98,14 @@ open class Contacts(
/**
* Get URI to the picture of the contact with the supplied email address.
*
* @param address
* An email address. The contact database is searched for a contact with this email
* address.
* @param emailAddress An email address, the contact database is searched for.
*
* @return URI to the picture of the contact with the supplied email address. `null` if
* no such contact could be found or the contact doesn't have a picture.
*/
fun getPhotoUri(address: String): Uri? {
fun getPhotoUri(emailAddress: EmailAddress): Uri? {
return try {
val cursor = getContactByAddress(address) ?: return null
val cursor = getContactFor(emailAddress) ?: return null
try {
if (!cursor.moveToFirst()) {
return null
@ -131,7 +119,7 @@ open class Contacts(
cursor.close()
}
} catch (e: Exception) {
Timber.e(e, "Couldn't fetch photo for contact with email $address")
Timber.e(e, "Couldn't fetch photo for contact with email ${emailAddress.address}")
null
}
}
@ -147,12 +135,12 @@ open class Contacts(
* Return a [Cursor] instance that can be used to fetch information
* about the contact with the given email address.
*
* @param address The email address to search for.
* @param emailAddress The email address to search for.
* @return A [Cursor] instance that can be used to fetch information
* about the contact with the given email address
*/
private fun getContactByAddress(address: String): Cursor? {
val uri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(address))
private fun getContactFor(emailAddress: EmailAddress): Cursor? {
val uri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress.address))
return if (hasContactPermission()) {
contentResolver.query(
uri,
@ -169,7 +157,7 @@ open class Contacts(
companion object {
/**
* The order in which the search results are returned by
* [.getContactByAddress].
* [.getContactBy].
*/
private const val SORT_ORDER = CommonDataKinds.Email.TIMES_CONTACTED + " DESC, " +
ContactsContract.Contacts.DISPLAY_NAME + ", " +
@ -199,7 +187,7 @@ open class Contacts(
private const val CONTACT_ID_INDEX = 2
private const val LOOKUP_KEY_INDEX = 4
private val nameCache = HashMap<String, String?>()
private val nameCache = HashMap<EmailAddress, String?>()
/**
* Clears the cache for names and photo uris

View file

@ -5,6 +5,7 @@ import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import app.k9mail.core.common.mail.EmailAddress
import com.fsck.k9.CoreResourceProvider
import com.fsck.k9.K9.contactNameColor
import com.fsck.k9.K9.isChangeContactNameColor
@ -99,7 +100,7 @@ class MessageHelper(
if (!showCorrespondentNames) {
return address.address
} else if (contacts != null) {
val name = contacts.getNameForAddress(address.address)
val name = contacts.getNameFor(EmailAddress(address.address))
if (name != null) {
return if (changeContactNameColor) {
val coloredName = SpannableString(name)

View file

@ -3,6 +3,7 @@ package com.fsck.k9.helper
import android.content.Context
import android.graphics.Color
import android.text.SpannableString
import app.k9mail.core.common.mail.EmailAddress
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
@ -24,8 +25,8 @@ class MessageHelperTest : RobolectricTest() {
val context: Context = RuntimeEnvironment.getApplication()
contacts = Contacts(context)
contactsWithFakeContact = object : Contacts(context) {
override fun getNameForAddress(address: String?): String? {
return if ("test@testor.com" == address) {
override fun getNameFor(emailAddress: EmailAddress): String? {
return if ("test@testor.com" == emailAddress.address) {
"Tim Testor"
} else {
null
@ -33,8 +34,8 @@ class MessageHelperTest : RobolectricTest() {
}
}
contactsWithFakeSpoofContact = object : Contacts(context) {
override fun getNameForAddress(address: String?): String? {
return if ("test@testor.com" == address) {
override fun getNameFor(emailAddress: EmailAddress): String? {
return if ("test@testor.com" == emailAddress.address) {
"Tim@Testor"
} else {
null

View file

@ -1,5 +1,6 @@
package com.fsck.k9.notification
import app.k9mail.core.common.mail.EmailAddress
import com.fsck.k9.Account
import com.fsck.k9.K9
import com.fsck.k9.helper.Contacts
@ -84,7 +85,9 @@ class K9NotificationStrategy(private val contacts: Contacts) : NotificationStrat
return false
}
if (account.isNotifyContactsMailOnly && !contacts.isAnyInContacts(message.from)) {
if (account.isNotifyContactsMailOnly &&
!contacts.isAnyInContacts(message.from.map { EmailAddress(it.address) })
) {
Timber.v("No notification: Message is not from a known contact")
return false
}

View file

@ -3,12 +3,13 @@ package com.fsck.k9.contacts
import android.content.ContentResolver
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import app.k9mail.core.common.mail.EmailAddress
import com.fsck.k9.helper.Contacts
import timber.log.Timber
internal class ContactPhotoLoader(private val contentResolver: ContentResolver, private val contacts: Contacts) {
fun loadContactPhoto(emailAddress: String): Bitmap? {
val photoUri = contacts.getPhotoUri(emailAddress) ?: return null
val photoUri = contacts.getPhotoUri(EmailAddress(emailAddress)) ?: return null
return try {
contentResolver.openInputStream(photoUri).use { inputStream ->
BitmapFactory.decodeStream(inputStream)

View file

@ -4,6 +4,7 @@ import android.app.PendingIntent
import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.k9mail.core.common.mail.EmailAddress
import com.fsck.k9.Account
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.helper.ClipboardManager
@ -103,7 +104,7 @@ internal class MessageDetailsViewModel(
Participant(
displayName = displayName,
emailAddress = emailAddress,
contactLookupUri = contacts.getContactUri(emailAddress),
contactLookupUri = contacts.getContactUri(EmailAddress(emailAddress)),
)
}
}

View file

@ -17,6 +17,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import app.k9mail.core.common.mail.EmailAddress
import com.fsck.k9.Account
import com.fsck.k9.Account.ShowPictures
import com.fsck.k9.helper.Contacts
@ -261,7 +262,7 @@ class MessageTopView(
return false
}
val senderEmailAddress = getSenderEmailAddress(message) ?: return false
return contacts.isInContacts(senderEmailAddress)
return contacts.isInContacts(EmailAddress(senderEmailAddress))
}
private fun getSenderEmailAddress(message: Message): String? {

View file

@ -528,10 +528,8 @@
<ID>ReturnCount:AutocryptDraftStateHeaderParser.kt$AutocryptDraftStateHeaderParser$fun parseAutocryptDraftStateHeader(headerValue: String): AutocryptDraftStateHeader?</ID>
<ID>ReturnCount:ChooseFolderActivity.kt$ChooseFolderActivity$private fun decodeArguments(savedInstanceState: Bundle?): Boolean</ID>
<ID>ReturnCount:CommandSetFlag.kt$CommandSetFlag$fun setFlag(folderServerId: String, messageServerIds: List&lt;String&gt;, flag: Flag, newState: Boolean)</ID>
<ID>ReturnCount:Contacts.kt$Contacts$fun getContactUri(emailAddress: String): Uri?</ID>
<ID>ReturnCount:Contacts.kt$Contacts$fun getPhotoUri(address: String): Uri?</ID>
<ID>ReturnCount:Contacts.kt$Contacts$fun isAnyInContacts(addresses: Array&lt;Address&gt;?): Boolean</ID>
<ID>ReturnCount:Contacts.kt$Contacts$open fun getNameForAddress(address: String?): String?</ID>
<ID>ReturnCount:Contacts.kt$Contacts$fun getContactUri(emailAddress: EmailAddress): Uri?</ID>
<ID>ReturnCount:Contacts.kt$Contacts$fun getPhotoUri(emailAddress: EmailAddress): Uri?</ID>
<ID>ReturnCount:DecoderUtil.kt$DecoderUtil$@JvmStatic fun decodeEncodedWords(body: String, message: Message?): String</ID>
<ID>ReturnCount:DecoderUtil.kt$DecoderUtil$private fun extractEncodedWord(body: String, begin: Int, end: Int, message: Message?): EncodedWord?</ID>
<ID>ReturnCount:EditIdentity.kt$EditIdentity$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>

View file

@ -0,0 +1,8 @@
package app.k9mail.core.common.mail
@JvmInline
value class EmailAddress(val address: String) {
init {
require(address.isNotBlank()) { "Email address must not be blank" }
}
}

View file

@ -0,0 +1,29 @@
package app.k9mail.core.common.mail
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlin.test.Test
import kotlin.test.assertFails
internal class EmailAddressTest {
@Test
fun `should reject blank email address`() {
assertFails("Email address must not be blank") {
EmailAddress("")
}
}
@Test
fun `should return email address`() {
val emailAddress = EmailAddress(EMAIL_ADDRESS)
val address = emailAddress.address
assertThat(address).isEqualTo(EMAIL_ADDRESS)
}
private companion object {
private const val EMAIL_ADDRESS = "email@example.com"
}
}