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
113 changes: 92 additions & 21 deletions FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ import com.flowcrypt.email.api.email.model.AttachmentInfo
import com.flowcrypt.email.api.email.model.IncomingMessageInfo
import com.flowcrypt.email.api.email.model.LocalFolder
import com.flowcrypt.email.api.email.model.OutgoingMessageInfo
import com.flowcrypt.email.api.retrofit.node.NodeRetrofitHelper
import com.flowcrypt.email.api.retrofit.node.NodeService
import com.flowcrypt.email.api.retrofit.request.node.ComposeEmailRequest
import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails
import com.flowcrypt.email.database.entity.AccountEntity
import com.flowcrypt.email.model.MessageEncryptionType
import com.flowcrypt.email.model.MessageType
import com.flowcrypt.email.security.KeyStoreCryptoManager
import com.flowcrypt.email.security.SecurityUtils
import com.flowcrypt.email.security.pgp.PgpEncrypt
import com.flowcrypt.email.util.GeneralUtil
import com.flowcrypt.email.util.SharedPreferencesHelper
import com.flowcrypt.email.util.exception.ExceptionUtil
import com.flowcrypt.email.util.exception.NodeEncryptException
import com.google.android.gms.auth.GoogleAuthException
import com.google.android.gms.auth.GoogleAuthUtil
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
Expand All @@ -53,6 +53,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.IOUtils
import java.io.ByteArrayInputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
Expand All @@ -67,6 +68,7 @@ import javax.mail.Multipart
import javax.mail.Part
import javax.mail.Session
import javax.mail.UIDFolder
import javax.mail.internet.AddressException
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeBodyPart
import javax.mail.internet.MimeMessage
Expand Down Expand Up @@ -623,28 +625,24 @@ class EmailUtil {
}

/**
* Generate a raw MIME message. Don't call it in the main thread.
* Generate a message(new, reply or forward). Don't call it in the main thread.
*
* @param info The given [OutgoingMessageInfo] which contains information about an outgoing
* message.
* @param pubKeys The public keys which will be used to generate an encrypted part.
* @return The generated raw MIME message.
*/
fun genRawMsgWithoutAtts(info: OutgoingMessageInfo, pubKeys: List<String>?): String {

val retrofit = NodeRetrofitHelper.getRetrofit() ?: return ""

val nodeService = retrofit.create(NodeService::class.java)
val request = ComposeEmailRequest(info, pubKeys)

val response = nodeService.composeEmail(request).execute()
val result = response.body() ?: throw NullPointerException("ComposeEmailResult == null")
fun genMessage(context: Context, info: OutgoingMessageInfo, pubKeys: List<String>? = null): Message {
val session = Session.getInstance(Properties())
return when (info.messageType) {
MessageType.NEW, MessageType.FORWARD -> {
prepareNewMsg(session, info, pubKeys)
}

if (result.apiError != null) {
throw NodeEncryptException(result.apiError)
MessageType.REPLY, MessageType.REPLY_ALL -> {
prepareReplyMsg(info, context, session, pubKeys)
}
}

return result.mimeMsg
}

/**
Expand Down Expand Up @@ -702,7 +700,7 @@ class EmailUtil {
}

private fun genMsgWithBackupTemplate(context: Context, account: AccountEntity, session: Session): Message {
val msg = MimeMessage(session)
val msg = FlowCryptMimeMessage(session)

msg.setFrom(InternetAddress(account.email))
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(account.email))
Expand Down Expand Up @@ -804,13 +802,17 @@ class EmailUtil {
* @return The reply quotes text
*/
@SuppressLint("SimpleDateFormat") // for now we use iso format, regardles of locality
fun prepareReplyQuotes(msgInfo: IncomingMessageInfo?): String {
fun genReplyContent(msgInfo: IncomingMessageInfo?): String {
val date = if (msgInfo != null) SimpleDateFormat("yyyy-MM-dd' at 'HH:mm").format(msgInfo.getReceiveDate()) else "unknown date"
val sender = msgInfo?.getFrom()?.firstOrNull()?.toString() ?: "unknown sender"
val replyText = msgInfo?.text?.replace("(?m)^".toRegex(), "> ") ?: "(unknown content)"
val replyText = prepareReplyQuotes(msgInfo?.text)
return "\n\nOn $date, $sender wrote:\n$replyText"
}

fun prepareReplyQuotes(originalText: String?): String {
return originalText?.replace("(?m)^".toRegex(), "> ") ?: "(unknown content)"
}

suspend fun patchingSecurityProviderSuspend(context: Context) = withContext(Dispatchers.IO) {
patchingSecurityProvider(context)
}
Expand Down Expand Up @@ -943,6 +945,41 @@ class EmailUtil {
return parameters.joinToString(separator = " ")
}

fun parseAddresses(fromAddress: String?): List<InternetAddress> {
return try {
InternetAddress.parse(fromAddress ?: "").toList()
} catch (e: AddressException) {
emptyList()
}
}

fun prepareNewMsg(session: Session, info: OutgoingMessageInfo,
pubKeys: List<String>? = null): MimeMessage {
val msg = FlowCryptMimeMessage(session)
msg.subject = info.subject
msg.setFrom(InternetAddress(info.from))
msg.setRecipients(Message.RecipientType.TO, info.toRecipients.toTypedArray())
msg.setRecipients(Message.RecipientType.CC, info.ccRecipients?.toTypedArray())
msg.setRecipients(Message.RecipientType.BCC, info.bccRecipients?.toTypedArray())
val bodyPart = MimeBodyPart()
bodyPart.setText(prepareMsgContent(info, pubKeys))
val mimeMultipart = MimeMultipart()
mimeMultipart.addBodyPart(bodyPart)
msg.setContent(mimeMultipart)
return msg
}

fun genReplyMessage(replyToMsg: MimeMessage,
info: OutgoingMessageInfo, pubKeys: List<String>? = null): Message {
val reply = replyToMsg.reply(false)//we use replyToAll == false to use the own logic
reply.setFrom(InternetAddress(info.from))
reply.setText(prepareMsgContent(info, pubKeys))
reply.setRecipients(Message.RecipientType.TO, info.toRecipients.toTypedArray())
reply.setRecipients(Message.RecipientType.CC, info.ccRecipients?.toTypedArray())
reply.setRecipients(Message.RecipientType.BCC, info.bccRecipients?.toTypedArray())
return reply
}

private fun generateNonGmailSearchTerm(localFolder: LocalFolder): SearchTerm {
return OrTerm(arrayOf(
SubjectTerm(localFolder.searchQuery),
Expand All @@ -953,5 +990,39 @@ class EmailUtil {
RecipientStringTerm(Message.RecipientType.BCC, localFolder.searchQuery)
))
}

private fun prepareReplyMsg(info: OutgoingMessageInfo, context: Context,
session: Session, pubKeys: List<String>?): Message {
val replyToMessageEntity = info.replyToMsgEntity
?: throw IllegalArgumentException("Empty replyTo MessageEntity")
var msg: MimeMessage
if (replyToMessageEntity.rawMessageWithoutAttachments.isNullOrEmpty()) {
val snapshot = MsgsCacheManager.getMsgSnapshot(replyToMessageEntity.id.toString())
?: throw IllegalArgumentException("Snapshot of replyTo message not found")

val uri = snapshot.getUri(0) ?: throw IllegalArgumentException("Uri not found")
val input = context.contentResolver?.openInputStream(uri)
?: throw IllegalArgumentException("InputStream not found")
msg = FlowCryptMimeMessage(session, KeyStoreCryptoManager.getCipherInputStream(input))
} else {
val input = ByteArrayInputStream(replyToMessageEntity.rawMessageWithoutAttachments.toByteArray())
try {
msg = FlowCryptMimeMessage(session, KeyStoreCryptoManager.getCipherInputStream(input))
} catch (e: Exception) {
//added for compatibility to previous versions
msg = FlowCryptMimeMessage(session, input)
}
}

return genReplyMessage(msg, info, pubKeys)
}

private fun prepareMsgContent(info: OutgoingMessageInfo, pubKeys: List<String>?): String {
return if (info.encryptionType == MessageEncryptionType.ENCRYPTED) {
PgpEncrypt.encryptMsg(info.msg ?: "", pubKeys ?: emptyList())
} else {
info.msg ?: ""
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
*/

package com.flowcrypt.email.api.email

import com.flowcrypt.email.extensions.javax.mail.internet.domain
import java.io.InputStream
import javax.mail.Session
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage

/**
* A custom realization of [MimeMessage] that overrides <code>MessageID</code> header
* to fit the following schema: "<unique_id@from.host.domain>" or "<unique_id@host>"
* if the from address is not specified. Where "host" is extracted
* from [InternetAddress.getLocalAddress]
*
* @author Denis Bondarenko
* Date: 3/30/21
* Time: 9:30 AM
* E-mail: DenBond7@gmail.com
*/
open class FlowCryptMimeMessage : MimeMessage {
constructor(session: Session) : super(session)
constructor(session: Session, inputStream: InputStream) : super(session, inputStream)
constructor(mimeMessage: MimeMessage) : super(mimeMessage)

override fun updateMessageID() {
super.updateMessageID()
val from = from?.firstOrNull() ?: return
val domain = (from as? InternetAddress)?.domain ?: return
val originalMessageId = getHeader("Message-ID").firstOrNull() ?: return
val modifiedMessageId = originalMessageId.replace("@.*>\$".toRegex(), "@$domain>")
setHeader("Message-ID", modifiedMessageId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,18 @@ data class IncomingMessageInfo constructor(val msgEntity: MessageEntity,
var text: String? = null,
var inlineSubject: String? = null,
val msgBlocks: List<@JvmSuppressWildcards MsgBlock>? = null,
val origMsgHeaders: String? = null,
val encryptionType: MessageEncryptionType) : Parcelable {
fun getSubject(): String? = msgEntity.subject

fun getFrom(): List<InternetAddress>? = msgEntity.from
fun getFrom(): List<InternetAddress> = msgEntity.from

fun getReplyTo(): List<InternetAddress>? = msgEntity.replyToAddress
fun getReplyTo(): List<InternetAddress> = msgEntity.replyToAddress

fun getReceiveDate(): Date = Date(msgEntity.receivedDate ?: 0)

fun getTo(): List<InternetAddress>? = msgEntity.to
fun getTo(): List<InternetAddress> = msgEntity.to

fun getCc(): List<InternetAddress>? = msgEntity.cc
fun getCc(): List<InternetAddress> = msgEntity.cc

fun getHtmlMsgBlock(): MsgBlock? {
for (part in msgBlocks ?: emptyList()) {
Expand All @@ -56,14 +55,13 @@ data class IncomingMessageInfo constructor(val msgEntity: MessageEntity,
fun getUid(): Int = msgEntity.uid.toInt()

constructor(msgEntity: MessageEntity, text: String?, subject: String?, msgBlocks: List<MsgBlock>,
origMsgHeaders: String?, encryptionType: MessageEncryptionType) : this(
encryptionType: MessageEncryptionType) : this(
msgEntity,
null,
null,
text,
subject,
msgBlocks,
origMsgHeaders,
encryptionType)

fun hasHtmlText(): Boolean {
Expand Down Expand Up @@ -91,7 +89,6 @@ data class IncomingMessageInfo constructor(val msgEntity: MessageEntity,
source.readString(),
source.readString(),
mutableListOf<MsgBlock>().apply { source.readTypedList(this, GenericMsgBlock.CREATOR) },
source.readString(),
source.readParcelable(MessageEncryptionType::class.java.classLoader)!!
)

Expand All @@ -104,7 +101,6 @@ data class IncomingMessageInfo constructor(val msgEntity: MessageEntity,
writeString(text)
writeString(inlineSubject)
writeTypedList(msgBlocks)
writeString(origMsgHeaders)
writeParcelable(encryptionType, flags)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ package com.flowcrypt.email.api.email.model

import android.os.Parcel
import android.os.Parcelable
import com.flowcrypt.email.database.entity.MessageEntity
import com.flowcrypt.email.model.MessageEncryptionType
import com.flowcrypt.email.model.MessageType
import javax.mail.internet.InternetAddress

/**
* Simple POJO class which describe an outgoing message model.
Expand All @@ -21,15 +24,15 @@ data class OutgoingMessageInfo constructor(
val account: String,
val subject: String,
val msg: String? = null,
val toRecipients: List<String>? = null,
val ccRecipients: List<String>? = null,
val bccRecipients: List<String>? = null,
val toRecipients: List<InternetAddress>,
val ccRecipients: List<InternetAddress>? = null,
val bccRecipients: List<InternetAddress>? = null,
val from: String,
val origMsgHeaders: String? = null,
val atts: List<AttachmentInfo>? = null,
val forwardedAtts: List<AttachmentInfo>? = null,
val encryptionType: MessageEncryptionType,
val isForwarded: Boolean = false,
val messageType: MessageType,
val replyToMsgEntity: MessageEntity? = null,
val uid: Long = 0) : Parcelable {

/**
Expand All @@ -38,26 +41,26 @@ data class OutgoingMessageInfo constructor(
* @return A list of the all recipients
*/
fun getAllRecipients(): List<String> {
val recipients = mutableListOf<String>()
toRecipients?.let { recipients.addAll(it) }
ccRecipients?.let { recipients.addAll(it) }
bccRecipients?.let { recipients.addAll(it) }
return recipients
val allRecipients = mutableListOf<String>()
allRecipients.addAll(toRecipients.map { address -> address.address })
ccRecipients?.let { allRecipients.addAll(it.map { address -> address.address }) }
bccRecipients?.let { allRecipients.addAll(it.map { address -> address.address }) }
return allRecipients
}

constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readString()!!,
parcel.readString(),
parcel.createStringArrayList(),
parcel.createStringArrayList(),
parcel.createStringArrayList(),
mutableListOf<InternetAddress>().apply { parcel.readList(this as List<*>, InternetAddress::class.java.classLoader) },
mutableListOf<InternetAddress>().apply { parcel.readList(this as List<*>, InternetAddress::class.java.classLoader) },
mutableListOf<InternetAddress>().apply { parcel.readList(this as List<*>, InternetAddress::class.java.classLoader) },
parcel.readString()!!,
parcel.readString(),
mutableListOf<AttachmentInfo>().apply { parcel.readTypedList(this, AttachmentInfo.CREATOR) },
mutableListOf<AttachmentInfo>().apply { parcel.readTypedList(this, AttachmentInfo.CREATOR) },
parcel.readParcelable<MessageEncryptionType>(MessageEncryptionType::class.java.classLoader)!!,
parcel.readByte() != 0.toByte(),
parcel.readParcelable<MessageType>(MessageType::class.java.classLoader)!!,
parcel.readParcelable<MessageEntity>(MessageEntity::class.java.classLoader),
parcel.readLong())

override fun describeContents(): Int {
Expand All @@ -69,15 +72,15 @@ data class OutgoingMessageInfo constructor(
writeString(account)
writeString(subject)
writeString(msg)
writeStringList(toRecipients)
writeStringList(ccRecipients)
writeStringList(bccRecipients)
writeList(toRecipients)
writeList(ccRecipients)
writeList(bccRecipients)
writeString(from)
writeString(origMsgHeaders)
writeTypedList(atts)
writeTypedList(forwardedAtts)
writeParcelable(encryptionType, flags)
writeByte(if (isForwarded) 1.toByte() else 0.toByte())
writeParcelable(messageType, flags)
writeParcelable(replyToMsgEntity, flags)
writeLong(uid)
}
}
Expand Down
Loading