Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
*/

package com.flowcrypt.email.jetpack.workmanager

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.ListenableWorker
import androidx.work.testing.TestListenableWorkerBuilder
import androidx.work.workDataOf
import com.flowcrypt.email.database.FlowCryptRoomDatabase
import com.flowcrypt.email.database.entity.RecipientEntity
import kotlinx.coroutines.runBlocking
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/**
* @author Denis Bondarenko
* Date: 5/30/22
* Time: 2:36 PM
* E-mail: DenBond7@gmail.com
*/
@RunWith(AndroidJUnit4::class)
class EmailAndNameWorkerTest {
private lateinit var context: Context

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}

@Test
fun testUpdateExistingRecipient() {
val recipientEmail = "user@flowcrypt.test"
val recipientName = "Bob Alisa"
val recipientDao = FlowCryptRoomDatabase.getDatabase(context).recipientDao()
val worker = TestListenableWorkerBuilder<EmailAndNameWorker>(
context = context,
inputData = workDataOf(
EmailAndNameWorker.EXTRA_KEY_EMAILS to arrayOf(recipientEmail),
EmailAndNameWorker.EXTRA_KEY_NAMES to arrayOf(recipientName)
)
).build()
runBlocking {
recipientDao.insertSuspend(RecipientEntity(email = recipientEmail))
val existingUserBeforeUpdating = recipientDao.getRecipientByEmail(recipientEmail)
assertNotNull(existingUserBeforeUpdating)
//check that a user have no name before updating
assertNull(existingUserBeforeUpdating?.name)
val result = worker.doWork()
assertThat(result, `is`(ListenableWorker.Result.success()))
//check that a user have some name after updating
val existingUserAfterUpdating = recipientDao.getRecipientByEmail(recipientEmail)
assertNotNull(existingUserAfterUpdating)
assertEquals(recipientName, existingUserAfterUpdating?.name)
}
}

@Test
fun testCreateNewRecipient() {
val recipientEmail = "user@flowcrypt.test"
val recipientName = "Bob Alisa"
val recipientDao = FlowCryptRoomDatabase.getDatabase(context).recipientDao()
val worker = TestListenableWorkerBuilder<EmailAndNameWorker>(
context = context,
inputData = workDataOf(
EmailAndNameWorker.EXTRA_KEY_EMAILS to arrayOf(recipientEmail),
EmailAndNameWorker.EXTRA_KEY_NAMES to arrayOf(recipientName)
)
).build()
runBlocking {
//check that a user is not exist
val existingUserBeforeUpdating = recipientDao.getRecipientByEmail(recipientEmail)
assertNull(existingUserBeforeUpdating)

val result = worker.doWork()
assertThat(result, `is`(ListenableWorker.Result.success()))

//check that a new user was added
val existingUserAfterUpdating = recipientDao.getRecipientByEmail(recipientEmail)
assertNotNull(existingUserAfterUpdating)
assertEquals(recipientName, existingUserAfterUpdating?.name)
}
}
}
5 changes: 0 additions & 5 deletions FlowCrypt/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,6 @@
android:name=".service.PassPhrasesInRAMService"
android:exported="false" />

<service
android:name=".service.EmailAndNameUpdaterService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />

<service
android:name=".service.IdleService"
android:exported="false" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ import com.flowcrypt.email.database.entity.AttachmentEntity
import com.flowcrypt.email.database.entity.LabelEntity
import com.flowcrypt.email.database.entity.MessageEntity
import com.flowcrypt.email.extensions.kotlin.toHex
import com.flowcrypt.email.jetpack.workmanager.EmailAndNameWorker
import com.flowcrypt.email.jetpack.workmanager.sync.CheckIsLoadedMessagesEncryptedWorker
import com.flowcrypt.email.model.EmailAndNamePair
import com.flowcrypt.email.service.EmailAndNameUpdaterService
import com.flowcrypt.email.service.MessagesNotificationManager
import com.flowcrypt.email.util.FileAndDirectoryUtils
import com.flowcrypt.email.util.GeneralUtil
Expand Down Expand Up @@ -576,12 +575,12 @@ class MessagesViewModel(application: Application) : AccountViewModel(application
val isSentFolder = imapFolder?.attributes?.contains("\\Sent") ?: true

if (isSentFolder) {
val emailAndNamePairs = ArrayList<EmailAndNamePair>()
val emailAndNamePairs = mutableListOf<Pair<String, String?>>()
for (message in messages) {
emailAndNamePairs.addAll(getEmailAndNamePairs(message))
}

EmailAndNameUpdaterService.enqueueWork(getApplication(), emailAndNamePairs)
EmailAndNameWorker.enqueue(getApplication(), emailAndNamePairs)
}
} catch (e: MessagingException) {
e.printStackTrace()
Expand All @@ -590,31 +589,31 @@ class MessagesViewModel(application: Application) : AccountViewModel(application
}

/**
* Generate a list of [EmailAndNamePair] objects from the input message.
* Generate a list of [Pair] objects from the input message.
* This information will be retrieved from "to" and "cc" headers.
*
* @param msg The input [javax.mail.Message].
* @return <tt>[List]</tt> of EmailAndNamePair objects, which contains information
* @param msg The input [jakarta.mail.Message].
* @return <tt>[List]</tt> of [Pair] objects, which contains information
* about
* emails and names.
* @throws MessagingException when retrieve information about recipients.
*/
private fun getEmailAndNamePairs(msg: Message): List<EmailAndNamePair> {
val pairs = ArrayList<EmailAndNamePair>()
private fun getEmailAndNamePairs(msg: Message): List<Pair<String, String>> {
val pairs = mutableListOf<Pair<String, String>>()

val addressesTo = msg.getRecipients(Message.RecipientType.TO)
if (addressesTo != null) {
for (address in addressesTo) {
val internetAddress = address as InternetAddress
pairs.add(EmailAndNamePair(internetAddress.address, internetAddress.personal))
pairs.add(Pair(internetAddress.address, internetAddress.personal))
}
}

val addressesCC = msg.getRecipients(Message.RecipientType.CC)
if (addressesCC != null) {
for (address in addressesCC) {
val internetAddress = address as InternetAddress
pairs.add(EmailAndNamePair(internetAddress.address, internetAddress.personal))
pairs.add(Pair(internetAddress.address, internetAddress.personal))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: denbond7
*/

package com.flowcrypt.email.jetpack.workmanager

import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.flowcrypt.email.BuildConfig
import com.flowcrypt.email.database.FlowCryptRoomDatabase
import com.flowcrypt.email.database.dao.RecipientDao
import com.flowcrypt.email.database.entity.RecipientEntity
import com.flowcrypt.email.jetpack.workmanager.sync.BaseSyncWorker

/**
* This service updates a name of some email entry or creates a new email entry if it not exist.
*
* Used a next logic:
* * if email in db:
* * if db_row.name is null and bool(name) == true:
* "save that person's name into the existing DB record"
*
* * else:
* "save that email, name pair into DB like so: new RecipientEntity(email, name);"
*
* @author DenBond7
* Date: 22.05.2017
* Time: 22:25
* E-mail: DenBond7@gmail.com
*/
class EmailAndNameWorker(context: Context, params: WorkerParameters) : BaseWorker(context, params) {
private var recipientDao: RecipientDao = FlowCryptRoomDatabase.getDatabase(context).recipientDao()

override suspend fun doWork(): Result {
val emails = inputData.getStringArray(EXTRA_KEY_EMAILS) ?: return Result.failure()
val names = inputData.getStringArray(EXTRA_KEY_NAMES) ?: return Result.failure()

if (emails.size != names.size) return Result.failure()

for (i in emails.indices) {
val email = emails[i].lowercase()
val name = names[i]
val recipientEntity = recipientDao.getRecipientByEmailSuspend(email)
if (recipientEntity != null) {
if (recipientEntity.name.isNullOrEmpty()) {
recipientDao.updateSuspend(recipientEntity.copy(name = name))
}
} else {
recipientDao.insertSuspend(RecipientEntity(email = email, name = name))
}
}

return Result.success()
}

companion object {
const val EXTRA_KEY_EMAILS = BuildConfig.APPLICATION_ID + ".EXTRA_KEY_EMAILS"
const val EXTRA_KEY_NAMES = BuildConfig.APPLICATION_ID + ".EXTRA_KEY_NAMES"
const val GROUP_UNIQUE_TAG = BuildConfig.APPLICATION_ID + ".UPDATE_EMAIL_AND_NAME"

fun enqueue(context: Context, emailAndNamePairs: Collection<Pair<String, String?>>) {
val emails = emailAndNamePairs.map { it.first }
val names = emailAndNamePairs.map { it.second }

WorkManager
.getInstance(context.applicationContext)
.enqueueUniqueWork(
GROUP_UNIQUE_TAG,
ExistingWorkPolicy.APPEND,
OneTimeWorkRequestBuilder<EmailAndNameWorker>()
.addTag(BaseSyncWorker.TAG_SYNC)
.setInputData(
workDataOf(
EXTRA_KEY_EMAILS to emails.toTypedArray(),
EXTRA_KEY_NAMES to names.toTypedArray()
)
)
.build()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import com.flowcrypt.email.api.email.gmail.api.GmaiAPIMimeMessage
import com.flowcrypt.email.database.FlowCryptRoomDatabase
import com.flowcrypt.email.database.entity.AccountEntity
import com.flowcrypt.email.database.entity.RecipientEntity
import com.flowcrypt.email.model.EmailAndNamePair
import com.sun.mail.imap.IMAPFolder
import jakarta.mail.FetchProfile
import jakarta.mail.Folder
Expand Down Expand Up @@ -152,7 +151,7 @@ class LoadRecipientsWorker(context: Context, params: WorkerParameters) :
}

private suspend fun updateContacts(msgs: Array<Message>) = withContext(Dispatchers.IO) {
val emailAndNamePairs = mutableListOf<EmailAndNamePair>()
val emailAndNamePairs = mutableListOf<Pair<String, String>>()
for (msg in msgs) {
emailAndNamePairs.addAll(parseRecipients(msg, Message.RecipientType.TO))
emailAndNamePairs.addAll(parseRecipients(msg, Message.RecipientType.CC))
Expand All @@ -176,22 +175,23 @@ class LoadRecipientsWorker(context: Context, params: WorkerParameters) :
}

for (emailAndNamePair in emailAndNamePairs) {
if (recipientsInDatabase.contains(emailAndNamePair.email)) {
val recipientEntity = recipientsByEmailMap[emailAndNamePair.email]
if (recipientsInDatabase.contains(emailAndNamePair.first)) {
val recipientEntity = recipientsByEmailMap[emailAndNamePair.first]
if (recipientEntity?.email.isNullOrEmpty()) {
if (!recipientsToUpdate.contains(emailAndNamePair.email)) {
emailAndNamePair.email?.let {
recipientsToUpdate.add(it)
}
recipientEntity?.copy(name = emailAndNamePair.name)?.let { updateCandidates.add(it) }
if (!recipientsToUpdate.contains(emailAndNamePair.first)) {
recipientsToUpdate.add(emailAndNamePair.first)
recipientEntity?.copy(name = emailAndNamePair.second)?.let { updateCandidates.add(it) }
}
}
} else {
if (!recipientsToCreate.contains(emailAndNamePair.email)) {
emailAndNamePair.email?.let {
recipientsToCreate.add(it)
newCandidates.add(RecipientEntity(email = it, name = emailAndNamePair.name))
}
if (!recipientsToCreate.contains(emailAndNamePair.first)) {
recipientsToCreate.add(emailAndNamePair.first)
newCandidates.add(
RecipientEntity(
email = emailAndNamePair.first,
name = emailAndNamePair.second
)
)
}
}
}
Expand All @@ -201,30 +201,26 @@ class LoadRecipientsWorker(context: Context, params: WorkerParameters) :
}

/**
* Generate an array of [EmailAndNamePair] objects from the input message.
* Generate an array of [Pair] objects from the input message.
* This information will be retrieved from "to", "cc" or "bcc" headers.
*
* @param msg The input [Message].
* @param recipientType The input [Message.RecipientType].
* @return An array of EmailAndNamePair objects, which contains information about emails and names.
* @return An array of [Pair] objects, which contains information about emails and names.
*/
private suspend fun parseRecipients(
msg: Message?,
recipientType: Message.RecipientType?
): List<EmailAndNamePair> = withContext(Dispatchers.IO) {
): List<Pair<String, String>> = withContext(Dispatchers.IO) {
if (msg != null && recipientType != null) {
try {
val header = msg.getHeader(recipientType.toString()) ?: return@withContext emptyList()
if (header.isNotEmpty()) {
if (!TextUtils.isEmpty(header[0])) {
val addresses = InternetAddress.parse(header[0])
val emailAndNamePairs = mutableListOf<EmailAndNamePair>()
val emailAndNamePairs = mutableListOf<Pair<String, String>>()
for (address in addresses) {
emailAndNamePairs.add(
EmailAndNamePair(
address.address.lowercase(), address.personal
)
)
emailAndNamePairs.add(Pair(address.address.lowercase(), address.personal))
}

return@withContext emailAndNamePairs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ class JobIdManager {
*/
const val JOB_MAX_ID = 10

const val JOB_TYPE_SYNC = 1
const val JOB_TYPE_ACTION_QUEUE = 2
const val JOB_TYPE_EMAIL_AND_NAME_UPDATE = 3
const val JOB_TYPE_PREPARE_OUT_GOING_MESSAGE = 4
const val JOB_TYPE_FEEDBACK = 5
}
Expand Down
Loading