diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt index 1d3635ddf3..d83f2be8f4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt @@ -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 @@ -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 @@ -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 @@ -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 { - - 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? = 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 } /** @@ -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)) @@ -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) } @@ -943,6 +945,41 @@ class EmailUtil { return parameters.joinToString(separator = " ") } + fun parseAddresses(fromAddress: String?): List { + return try { + InternetAddress.parse(fromAddress ?: "").toList() + } catch (e: AddressException) { + emptyList() + } + } + + fun prepareNewMsg(session: Session, info: OutgoingMessageInfo, + pubKeys: List? = 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? = 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), @@ -953,5 +990,39 @@ class EmailUtil { RecipientStringTerm(Message.RecipientType.BCC, localFolder.searchQuery) )) } + + private fun prepareReplyMsg(info: OutgoingMessageInfo, context: Context, + session: Session, pubKeys: List?): 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 { + return if (info.encryptionType == MessageEncryptionType.ENCRYPTED) { + PgpEncrypt.encryptMsg(info.msg ?: "", pubKeys ?: emptyList()) + } else { + info.msg ?: "" + } + } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/FlowCryptMimeMessage.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/FlowCryptMimeMessage.kt new file mode 100644 index 0000000000..1397fab291 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/FlowCryptMimeMessage.kt @@ -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 MessageID header + * to fit the following schema: "" or "" + * 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) + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/IncomingMessageInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/IncomingMessageInfo.kt index 95cbff22e2..a9fb5dee4e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/IncomingMessageInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/IncomingMessageInfo.kt @@ -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? = msgEntity.from + fun getFrom(): List = msgEntity.from - fun getReplyTo(): List? = msgEntity.replyToAddress + fun getReplyTo(): List = msgEntity.replyToAddress fun getReceiveDate(): Date = Date(msgEntity.receivedDate ?: 0) - fun getTo(): List? = msgEntity.to + fun getTo(): List = msgEntity.to - fun getCc(): List? = msgEntity.cc + fun getCc(): List = msgEntity.cc fun getHtmlMsgBlock(): MsgBlock? { for (part in msgBlocks ?: emptyList()) { @@ -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, - origMsgHeaders: String?, encryptionType: MessageEncryptionType) : this( + encryptionType: MessageEncryptionType) : this( msgEntity, null, null, text, subject, msgBlocks, - origMsgHeaders, encryptionType) fun hasHtmlText(): Boolean { @@ -91,7 +89,6 @@ data class IncomingMessageInfo constructor(val msgEntity: MessageEntity, source.readString(), source.readString(), mutableListOf().apply { source.readTypedList(this, GenericMsgBlock.CREATOR) }, - source.readString(), source.readParcelable(MessageEncryptionType::class.java.classLoader)!! ) @@ -104,7 +101,6 @@ data class IncomingMessageInfo constructor(val msgEntity: MessageEntity, writeString(text) writeString(inlineSubject) writeTypedList(msgBlocks) - writeString(origMsgHeaders) writeParcelable(encryptionType, flags) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt index 57e46a1410..6daf57d16c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt @@ -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. @@ -21,15 +24,15 @@ data class OutgoingMessageInfo constructor( val account: String, val subject: String, val msg: String? = null, - val toRecipients: List? = null, - val ccRecipients: List? = null, - val bccRecipients: List? = null, + val toRecipients: List, + val ccRecipients: List? = null, + val bccRecipients: List? = null, val from: String, - val origMsgHeaders: String? = null, val atts: List? = null, val forwardedAtts: List? = null, val encryptionType: MessageEncryptionType, - val isForwarded: Boolean = false, + val messageType: MessageType, + val replyToMsgEntity: MessageEntity? = null, val uid: Long = 0) : Parcelable { /** @@ -38,26 +41,26 @@ data class OutgoingMessageInfo constructor( * @return A list of the all recipients */ fun getAllRecipients(): List { - val recipients = mutableListOf() - toRecipients?.let { recipients.addAll(it) } - ccRecipients?.let { recipients.addAll(it) } - bccRecipients?.let { recipients.addAll(it) } - return recipients + val allRecipients = mutableListOf() + 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().apply { parcel.readList(this as List<*>, InternetAddress::class.java.classLoader) }, + mutableListOf().apply { parcel.readList(this as List<*>, InternetAddress::class.java.classLoader) }, + mutableListOf().apply { parcel.readList(this as List<*>, InternetAddress::class.java.classLoader) }, parcel.readString()!!, - parcel.readString(), mutableListOf().apply { parcel.readTypedList(this, AttachmentInfo.CREATOR) }, mutableListOf().apply { parcel.readTypedList(this, AttachmentInfo.CREATOR) }, parcel.readParcelable(MessageEncryptionType::class.java.classLoader)!!, - parcel.readByte() != 0.toByte(), + parcel.readParcelable(MessageType::class.java.classLoader)!!, + parcel.readParcelable(MessageEntity::class.java.classLoader), parcel.readLong()) override fun describeContents(): Int { @@ -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) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/NodeService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/NodeService.kt index b60287e55c..4d338de3bc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/NodeService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/NodeService.kt @@ -5,14 +5,10 @@ package com.flowcrypt.email.api.retrofit.node -import com.flowcrypt.email.api.retrofit.request.node.ComposeEmailRequest import com.flowcrypt.email.api.retrofit.request.node.DecryptFileRequest -import com.flowcrypt.email.api.retrofit.request.node.EncryptMsgRequest import com.flowcrypt.email.api.retrofit.request.node.ParseDecryptMsgRequest import com.flowcrypt.email.api.retrofit.request.node.VersionRequest -import com.flowcrypt.email.api.retrofit.response.node.ComposeEmailResult import com.flowcrypt.email.api.retrofit.response.node.DecryptedFileResult -import com.flowcrypt.email.api.retrofit.response.node.EncryptedMsgResult import com.flowcrypt.email.api.retrofit.response.node.ParseDecryptedMsgResult import com.flowcrypt.email.api.retrofit.response.node.VersionResult import retrofit2.Call @@ -33,12 +29,6 @@ interface NodeService { @POST("/") fun getVersion(@Body request: VersionRequest): Call - @POST("/") - fun encryptMsg(@Body request: EncryptMsgRequest): Call - - @POST("/") - fun composeEmail(@Body request: ComposeEmailRequest): Call - @POST("/") fun parseDecryptMsgOld(@Body request: ParseDecryptMsgRequest): Call diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/RequestsManager.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/RequestsManager.kt index 48abaacae0..f2e6bc587e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/RequestsManager.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/RequestsManager.kt @@ -10,7 +10,6 @@ import android.net.Uri import android.os.AsyncTask import androidx.lifecycle.LiveData import com.flowcrypt.email.api.retrofit.request.node.DecryptFileRequest -import com.flowcrypt.email.api.retrofit.request.node.EncryptMsgRequest import com.flowcrypt.email.api.retrofit.request.node.NodeRequest import com.flowcrypt.email.api.retrofit.request.node.NodeRequestWrapper import com.flowcrypt.email.api.retrofit.request.node.ParseDecryptMsgRequest @@ -19,7 +18,6 @@ import com.flowcrypt.email.api.retrofit.response.node.BaseNodeResponse import com.flowcrypt.email.api.retrofit.response.node.NodeResponseWrapper import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.jetpack.livedata.SingleLiveEvent -import com.flowcrypt.email.node.TestData /** * @author DenBond7 @@ -39,7 +37,7 @@ object RequestsManager { } fun encryptMsg(requestCode: Int, msg: String) { - load(requestCode, EncryptMsgRequest(msg, listOf(*TestData.mixedPubKeys))) + //load(requestCode, EncryptMsgRequest(msg, listOf(*TestData.mixedPubKeys))) } fun decryptMsg(requestCode: Int, data: ByteArray = ByteArray(0), uri: Uri? = null, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/ComposeEmailRequest.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/ComposeEmailRequest.kt deleted file mode 100644 index 314a785c2d..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/ComposeEmailRequest.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.api.retrofit.request.node - -import com.flowcrypt.email.api.email.model.OutgoingMessageInfo -import com.flowcrypt.email.model.MessageEncryptionType -import com.google.gson.annotations.Expose - -/** - * Using this class we can create a request to create a raw MIME message(encrypted or plain). - * - * @author Denis Bondarenko - * Date: 3/27/19 - * Time: 3:00 PM - * E-mail: DenBond7@gmail.com - */ -class ComposeEmailRequest(info: OutgoingMessageInfo?, - @field:Expose val pubKeys: List?) : BaseNodeRequest() { - - @Expose - private var format: String? = null - - @Expose - private var text: String? = null - - @Expose - private var to: List? = null - - @Expose - private var cc: List? = null - - @Expose - private var bcc: List? = null - - @Expose - private var from: String? = null - - @Expose - private var subject: String? = null - - @Expose - private var replyToMimeMsg: String? = null - //todo-denbond7 Ask Tom. Maybe we have to rename this field for better understanding. It contains only headers (not whole MIME) - - override val endpoint: String = "composeEmail" - - override val data: ByteArray - get() = ByteArray(0) - - init { - if (info != null) { - format = if (info.encryptionType === MessageEncryptionType.ENCRYPTED) FORMAT_ENCRYPT_INLINE else FORMAT_PLAIN - text = info.msg - to = info.toRecipients - cc = info.ccRecipients - bcc = info.bccRecipients - from = info.from - subject = info.subject - replyToMimeMsg = info.origMsgHeaders - } - } - - companion object { - private const val FORMAT_ENCRYPT_INLINE = "encrypt-inline" - private const val FORMAT_PLAIN = "plain" - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/EncryptMsgRequest.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/EncryptMsgRequest.kt deleted file mode 100644 index c86bf0b136..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/EncryptMsgRequest.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.api.retrofit.request.node - -import com.flowcrypt.email.api.retrofit.node.NodeService -import com.google.gson.annotations.Expose -import retrofit2.Response - -/** - * Using this class we can create a request to encrypt an input message using the given public keys. - * - * @author Denis Bondarenko - * Date: 1/11/19 - * Time: 12:48 PM - * E-mail: DenBond7@gmail.com - */ -class EncryptMsgRequest(private val msg: String?, - @Expose val pubKeys: List) : BaseNodeRequest() { - - override val endpoint: String = "encryptMsg" - - override val data: ByteArray - get() = msg?.toByteArray() ?: byteArrayOf() - - override fun getResponse(nodeService: NodeService): Response<*> { - return nodeService.encryptMsg(this).execute() - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/node/ComposeEmailResult.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/node/ComposeEmailResult.kt deleted file mode 100644 index b225076b7f..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/node/ComposeEmailResult.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.api.retrofit.response.node - -import android.os.Parcel -import android.os.Parcelable -import com.flowcrypt.email.api.retrofit.response.base.ApiError -import com.google.gson.annotations.Expose -import com.google.gson.annotations.SerializedName -import org.apache.commons.io.IOUtils -import java.io.BufferedInputStream -import java.io.IOException -import java.nio.charset.StandardCharsets - -/** - * It's a result for "composeEmail" requests. - * - * @author Denis Bondarenko - * Date: 3/27/19 - * Time: 3:00 PM - * E-mail: DenBond7@gmail.com - */ -data class ComposeEmailResult constructor(@SerializedName("error") - @Expose override val apiError: ApiError?, - var mimeMsg: String = "") : BaseNodeResponse, Parcelable { - override fun handleRawData(bufferedInputStream: BufferedInputStream) { - val bytes = IOUtils.toByteArray(bufferedInputStream) ?: return - - try { - mimeMsg = IOUtils.toString(bytes, StandardCharsets.UTF_8.displayName()) - } catch (e: IOException) { - e.printStackTrace() - } - } - - constructor(source: Parcel) : this( - source.readParcelable(ApiError::class.java.classLoader), - source.readString()!! - ) - - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { - writeParcelable(apiError, flags) - writeString(mimeMsg) - } - - companion object { - @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): ComposeEmailResult = ComposeEmailResult(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/node/EncryptedMsgResult.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/node/EncryptedMsgResult.kt deleted file mode 100644 index d4eb38a2d4..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/node/EncryptedMsgResult.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.api.retrofit.response.node - -import android.os.Parcel -import android.os.Parcelable -import com.flowcrypt.email.api.retrofit.response.base.ApiError - -import com.google.gson.annotations.Expose -import com.google.gson.annotations.SerializedName - -import org.apache.commons.io.IOUtils - -import java.io.BufferedInputStream -import java.io.IOException -import java.nio.charset.StandardCharsets - -/** - * It's a result for "encryptMsg" requests. - * - * @author Denis Bondarenko - * Date: 1/11/19 - * Time: 12:51 PM - * E-mail: DenBond7@gmail.com - */ -data class EncryptedMsgResult constructor(@SerializedName("error") - @Expose override val apiError: ApiError?, - var encryptedMsg: String? = null) : BaseNodeResponse { - override fun handleRawData(bufferedInputStream: BufferedInputStream) { - val bytes = IOUtils.toByteArray(bufferedInputStream) ?: return - - try { - encryptedMsg = IOUtils.toString(bytes, StandardCharsets.UTF_8.displayName()) - } catch (e: IOException) { - e.printStackTrace() - } - } - - constructor(source: Parcel) : this( - source.readParcelable(ApiError::class.java.classLoader), - source.readString() - ) - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(dest: Parcel, flags: Int) = - with(dest) { - writeParcelable(apiError, flags) - writeString(encryptedMsg) - } - - companion object { - @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): EncryptedMsgResult = EncryptedMsgResult(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index 263fd68fac..f04f80e85f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -78,16 +78,16 @@ data class MessageEntity( ) : Parcelable { @Ignore - val from: List = parseAddresses(fromAddress) + val from: List = EmailUtil.parseAddresses(fromAddress) @Ignore - val replyToAddress: List = parseAddresses(replyTo) + val replyToAddress: List = EmailUtil.parseAddresses(replyTo) @Ignore - val to: List = parseAddresses(toAddress) + val to: List = EmailUtil.parseAddresses(toAddress) @Ignore - val cc: List = parseAddresses(ccAddress) + val cc: List = EmailUtil.parseAddresses(ccAddress) @Ignore val msgState: MessageState = MessageState.generate(state ?: MessageState.NONE.value) @@ -173,15 +173,6 @@ data class MessageEntity( return JavaEmailConstants.FOLDER_OUTBOX.equals(folder, ignoreCase = true) } - private fun parseAddresses(fromAddress: String?): - List { - return try { - listOf(*InternetAddress.parse(fromAddress ?: "")) - } catch (e: AddressException) { - emptyList() - } - } - companion object CREATOR : Parcelable.Creator { const val TABLE_NAME = "messages" diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/javax/mail/internet/InternetAddressExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/javax/mail/internet/InternetAddressExt.kt new file mode 100644 index 0000000000..a04489e876 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/javax/mail/internet/InternetAddressExt.kt @@ -0,0 +1,17 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.extensions.javax.mail.internet + +import javax.mail.internet.InternetAddress + +/** + * @author Denis Bondarenko + * Date: 3/30/21 + * Time: 10:41 AM + * E-mail: DenBond7@gmail.com + */ +val InternetAddress.domain: String + get() = address.substring(address.indexOf('@') + 1) \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 450df38739..20898a0dad 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -48,20 +48,16 @@ import com.flowcrypt.email.ui.activity.SearchMessagesActivity import com.flowcrypt.email.util.CacheManager import com.flowcrypt.email.util.cache.DiskLruCache import com.flowcrypt.email.util.exception.ApiException -import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.SyncTaskTerminatedException import com.sun.mail.imap.IMAPBodyPart import com.sun.mail.imap.IMAPFolder import com.sun.mail.imap.IMAPMessage -import com.sun.mail.util.ASCIIUtility import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.apache.commons.io.IOUtils import org.bouncycastle.bcpg.ArmoredInputStream import java.io.BufferedInputStream -import java.io.ByteArrayInputStream import java.io.File import java.io.IOException import java.io.InputStream @@ -180,13 +176,11 @@ class MsgDetailsViewModel(val localFolder: LocalFolder, val messageEntity: Messa val parseDecryptedMsgResult = it.data if (parseDecryptedMsgResult != null) { try { - val msgHeadersAsString = getMsgHeaders() val msgInfo = IncomingMessageInfo( msgEntity = messageEntity, text = parseDecryptedMsgResult.text, subject = parseDecryptedMsgResult.subject, msgBlocks = parseDecryptedMsgResult.msgBlocks ?: emptyList(), - origMsgHeaders = msgHeadersAsString, encryptionType = parseDecryptedMsgResult.getMsgEncryptionType() ) Result.success(requestCode = it.requestCode, data = msgInfo) @@ -442,42 +436,6 @@ class MsgDetailsViewModel(val localFolder: LocalFolder, val messageEntity: Messa } } - private suspend fun getMsgHeaders(): String? = withContext(Dispatchers.IO) { - return@withContext if (messageEntity.isOutboxMsg()) { - ByteArrayInputStream(messageEntity.rawMessageWithoutAttachments?.toByteArray() - ?: ByteArray(0)).use { parseHeaders(it) } - } else { - MsgsCacheManager.getMsgSnapshot(messageEntity.id.toString())?.getUri(0)?.let { uri -> - val context: Context = getApplication() - return@withContext context.contentResolver.openInputStream(uri)?.use { parseHeaders(it, true) } - } - } - } - - /** - * We fetch the first 50Kb from the given input stream and extract headers. - */ - private suspend fun parseHeaders(inputStream: InputStream?, - isDataEncrypted: Boolean = false): String = withContext(Dispatchers.IO) { - inputStream ?: return@withContext "" - val d = ByteArray(50000) - try { - if (isDataEncrypted) { - KeyStoreCryptoManager.getCipherInputStream(inputStream).use { - IOUtils.read(it, d) - } - } else { - inputStream.use { - IOUtils.read(it, d) - } - } - } catch (e: Exception) { - e.printStackTrace() - ExceptionUtil.handleError(e) - } - EmailUtil.getHeadersFromRawMIME(ASCIIUtility.toString(d)) - } - private suspend fun loadMessageFromServer(messageEntity: MessageEntity): DiskLruCache.Snapshot = withContext(Dispatchers.IO) { val accountEntity = getActiveAccountSuspend() ?: throw java.lang.NullPointerException("Account is null") diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt index 7a062ce4be..accfd01411 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt @@ -254,7 +254,7 @@ class ForwardedAttachmentsDownloaderWorker(context: Context, params: WorkerParam withContext(Dispatchers.IO) { if (msgEntity.isEncrypted == true) { requireNotNull(pubKeys) - PgpEncrypt.encryptFile(srcInputStream, destFile.outputStream(), pubKeys) + PgpEncrypt.encrypt(srcInputStream, destFile.outputStream(), pubKeys) } else { FileUtils.copyInputStreamToFile(srcInputStream, destFile) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpEncrypt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpEncrypt.kt index dcc431e998..e8d2e8cf8c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpEncrypt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpEncrypt.kt @@ -5,13 +5,14 @@ package com.flowcrypt.email.security.pgp +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.pgpainless.PGPainless import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream import java.io.OutputStream -import org.bouncycastle.bcpg.ArmoredInputStream -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection -import org.pgpainless.PGPainless /** * @author Denis Bondarenko @@ -20,9 +21,31 @@ import org.pgpainless.PGPainless * E-mail: DenBond7@gmail.com */ object PgpEncrypt { + fun encryptMsg(msg: String, pubKeys: List): String { + val outputStreamForEncryptedSource = ByteArrayOutputStream() + encrypt( + srcInputStream = ByteArrayInputStream(msg.toByteArray()), + destOutputStream = outputStreamForEncryptedSource, + pubKeys = pubKeys, + doArmor = true) + return String(outputStreamForEncryptedSource.toByteArray()) + } + + fun encrypt(srcInputStream: InputStream, destOutputStream: OutputStream, + pubKeys: List, doArmor: Boolean = false) { + val byteArrayInputStream = ByteArrayInputStream(pubKeys.joinToString(separator = "\n").toByteArray()) + val pgpPublicKeyRingCollection = byteArrayInputStream.use { + ArmoredInputStream(it).use { armoredInputStream -> + PGPainless.readKeyRing().publicKeyRingCollection(armoredInputStream) + } + } + + encrypt(srcInputStream, destOutputStream, pgpPublicKeyRingCollection, doArmor) + } + @Throws(IOException::class) - fun encryptFile(srcInputStream: InputStream, destOutputStream: OutputStream, - pgpPublicKeyRingCollection: PGPPublicKeyRingCollection) { + fun encrypt(srcInputStream: InputStream, destOutputStream: OutputStream, + pgpPublicKeyRingCollection: PGPPublicKeyRingCollection, doArmor: Boolean = false) { srcInputStream.use { srcStream -> destOutputStream.use { outStream -> val builder = PGPainless.encryptAndOrSign() @@ -30,21 +53,11 @@ object PgpEncrypt { .toRecipients(pgpPublicKeyRingCollection) .usingSecureAlgorithms() - builder.doNotSign().noArmor().use { encryptionStream -> + val out = if (doArmor) builder.doNotSign().asciiArmor() else builder.doNotSign().noArmor() + out.use { encryptionStream -> srcStream.copyTo(encryptionStream) } } } } - - fun encryptFile(srcInputStream: InputStream, destOutputStream: OutputStream, pubKeys: List) { - val byteArrayInputStream = ByteArrayInputStream(pubKeys.joinToString(separator = "\n").toByteArray()) - val pgpPublicKeyRingCollection = byteArrayInputStream.use { - ArmoredInputStream(it).use { armoredInputStream -> - PGPainless.readKeyRing().publicKeyRingCollection(armoredInputStream) - } - } - - encryptFile(srcInputStream, destOutputStream, pgpPublicKeyRingCollection) - } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/PrepareOutgoingMessagesJobIntentService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/PrepareOutgoingMessagesJobIntentService.kt index 6ce244e728..69df63460b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/PrepareOutgoingMessagesJobIntentService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/PrepareOutgoingMessagesJobIntentService.kt @@ -16,9 +16,6 @@ import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.model.AttachmentInfo import com.flowcrypt.email.api.email.model.MessageFlag import com.flowcrypt.email.api.email.model.OutgoingMessageInfo -import com.flowcrypt.email.api.email.protocol.OpenStoreHelper -import com.flowcrypt.email.api.retrofit.node.NodeRetrofitHelper -import com.flowcrypt.email.api.retrofit.node.NodeService import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity @@ -28,6 +25,7 @@ import com.flowcrypt.email.jetpack.workmanager.ForwardedAttachmentsDownloaderWor import com.flowcrypt.email.jetpack.workmanager.MessagesSenderWorker import com.flowcrypt.email.jobscheduler.JobIdManager import com.flowcrypt.email.model.MessageEncryptionType +import com.flowcrypt.email.model.MessageType import com.flowcrypt.email.model.PgpContact import com.flowcrypt.email.security.SecurityUtils import com.flowcrypt.email.security.pgp.PgpEncrypt @@ -39,14 +37,13 @@ import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.ForceHandlingException import com.flowcrypt.email.util.exception.NoKeyAvailableException import org.apache.commons.io.FileUtils -import org.apache.commons.io.IOUtils import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException import java.io.InputStream -import java.nio.charset.StandardCharsets import java.util.* -import javax.mail.internet.MimeMessage +import javax.mail.Message /** * This service creates a new outgoing message using the given [OutgoingMessageInfo]. @@ -80,7 +77,6 @@ class PrepareOutgoingMessagesJobIntentService : JobIntentService() { ?: return val accountEntity = roomDatabase.accountDao().getAccount(outgoingMsgInfo.account.toLowerCase(Locale.US)) ?: return - val sess = OpenStoreHelper.getAccountSess(applicationContext, accountEntity) val uid = outgoingMsgInfo.uid val email = accountEntity.email @@ -104,13 +100,22 @@ class PrepareOutgoingMessagesJobIntentService : JobIntentService() { SecurityUtils.getRecipientsPubKeys(this, recipients, accountEntity, senderEmail) } else null - val rawMsg = EmailUtil.genRawMsgWithoutAtts(outgoingMsgInfo, pubKeys) - val mimeMsg = MimeMessage(sess, IOUtils.toInputStream(rawMsg, StandardCharsets.UTF_8)) + val msg = EmailUtil.genMessage(applicationContext, outgoingMsgInfo, pubKeys) val attsCacheDir = getAttsCacheDir() val msgAttsCacheDir = File(attsCacheDir, UUID.randomUUID().toString()) - val msgEntity = prepareMessageEntity(accountEntity, outgoingMsgInfo, uid, mimeMsg, rawMsg, msgAttsCacheDir) + val out = ByteArrayOutputStream() + msg.writeTo(out) + + //todo-denbond7 need to think about that. It'll be better to store a message as a file + val msgEntity = prepareMessageEntity( + accountEntity = accountEntity, + msgInfo = outgoingMsgInfo, + generatedUID = uid, + msg = msg, + rawMsg = String(out.toByteArray()), + attsCacheDir = msgAttsCacheDir) newMsgId = roomDatabase.msgDao().insert(msgEntity) if (newMsgId > 0) { @@ -203,14 +208,14 @@ class PrepareOutgoingMessagesJobIntentService : JobIntentService() { } private fun prepareMessageEntity(accountEntity: AccountEntity, msgInfo: OutgoingMessageInfo, generatedUID: Long, - mimeMsg: MimeMessage, rawMsg: String, attsCacheDir: File): MessageEntity { + msg: Message, rawMsg: String, attsCacheDir: File): MessageEntity { val messageEntity = MessageEntity.genMsgEntity(accountEntity.email, - JavaEmailConstants.FOLDER_OUTBOX, mimeMsg, generatedUID, false) + JavaEmailConstants.FOLDER_OUTBOX, msg, generatedUID, false) val hasAtts = msgInfo.atts?.isNotEmpty() == true || msgInfo.forwardedAtts?.isNotEmpty() == true val isEncrypted = msgInfo.encryptionType === MessageEncryptionType.ENCRYPTED - val msgStateValue = if (msgInfo.isForwarded) MessageState.NEW_FORWARDED.value else MessageState.NEW.value + val msgStateValue = if (msgInfo.messageType == MessageType.FORWARD) MessageState.NEW_FORWARDED.value else MessageState.NEW.value return messageEntity.copy( hasAttachments = hasAtts, @@ -227,7 +232,6 @@ class PrepareOutgoingMessagesJobIntentService : JobIntentService() { uid: Long, pubKeys: List?, attsCacheDir: File) { val cachedAtts = ArrayList() - val nodeService = NodeRetrofitHelper.getRetrofit()!!.create(NodeService::class.java) if (msgInfo.atts?.isNotEmpty() == true) { val outgoingAtts = msgInfo.atts.map { it.apply { @@ -265,7 +269,7 @@ class PrepareOutgoingMessagesJobIntentService : JobIntentService() { } requireNotNull(pubKeys) - PgpEncrypt.encryptFile(originalFileInputStream, encryptedTempFile.outputStream(), pubKeys) + PgpEncrypt.encrypt(originalFileInputStream, encryptedTempFile.outputStream(), pubKeys) val uri = FileProvider.getUriForFile(this, Constants.FILE_PROVIDER_AUTHORITY, encryptedTempFile) att.uri = uri att.name = encryptedTempFile.name diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/NodeTestActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/NodeTestActivity.kt index 7611d475f8..7e12a09b9b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/NodeTestActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/NodeTestActivity.kt @@ -18,7 +18,6 @@ import com.flowcrypt.email.api.retrofit.response.model.node.DecryptErrorMsgBlock import com.flowcrypt.email.api.retrofit.response.model.node.MsgBlock import com.flowcrypt.email.api.retrofit.response.node.BaseNodeResponse import com.flowcrypt.email.api.retrofit.response.node.DecryptedFileResult -import com.flowcrypt.email.api.retrofit.response.node.EncryptedMsgResult import com.flowcrypt.email.api.retrofit.response.node.NodeResponseWrapper import com.flowcrypt.email.api.retrofit.response.node.ParseDecryptedMsgResult import com.flowcrypt.email.api.retrofit.response.node.VersionResult @@ -119,11 +118,11 @@ class NodeTestActivity : AppCompatActivity(), View.OnClickListener, Observer { - val encryptMsgResult = responseWrapper.result as EncryptedMsgResult? + /*val encryptMsgResult = responseWrapper.result as EncryptedMsgResult? addResultLine("encrypt-msg", responseWrapper) encryptedMsg = encryptMsgResult!!.encryptedMsg requestsManager!!.decryptMsg(R.id.req_id_decrypt_msg_ecc, data = encryptedMsg!!.toByteArray() - , prvKeys = TestData.eccPrvKeyInfo()) + , prvKeys = TestData.eccPrvKeyInfo())*/ } R.id.req_id_decrypt_msg_ecc -> { @@ -337,7 +336,7 @@ class NodeTestActivity : AppCompatActivity(), View.OnClickListener, Observer? = null @@ -173,7 +171,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad var msg = editTextEmailMsg?.text.toString() if (messageType == MessageType.REPLY || messageType == MessageType.REPLY_ALL) { if (iBShowQuotedText?.visibility == View.VISIBLE) { - msg += EmailUtil.prepareReplyQuotes(msgInfo) + msg += EmailUtil.genReplyContent(msgInfo) } } @@ -181,19 +179,19 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad attachments?.forEachIndexed { index, attachmentInfo -> attachmentInfo.path = index.toString() } return OutgoingMessageInfo( - accountViewModel.activeAccountLiveData.value?.email ?: "", - editTextEmailSubject?.text.toString(), - msg, - recipientsTo?.chipValues, - recipientsCc?.chipValues, - recipientsBcc?.chipValues, - editTextFrom?.text.toString(), - msgInfo?.origMsgHeaders, - attachments, - forwardedAtts, - listener.msgEncryptionType, - messageType === MessageType.FORWARD, - EmailUtil.genOutboxUID(context) + account = accountViewModel.activeAccountLiveData.value?.email ?: "", + subject = editTextEmailSubject?.text.toString(), + msg = msg, + toRecipients = recipientsTo?.chipValues?.map { InternetAddress(it) } ?: emptyList(), + ccRecipients = recipientsCc?.chipValues?.map { InternetAddress(it) }, + bccRecipients = recipientsBcc?.chipValues?.map { InternetAddress(it) }, + from = editTextFrom?.text.toString(), + atts = attachments, + forwardedAtts = forwardedAtts, + encryptionType = listener.msgEncryptionType, + messageType = messageType, + replyToMsgEntity = msgInfo?.msgEntity, + uid = EmailUtil.genOutboxUID(context) ) } @@ -595,9 +593,9 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad R.id.iBShowQuotedText -> { val currentCursorPosition = editTextEmailMsg?.selectionStart ?: 0 if (editTextEmailMsg?.text?.isNotEmpty() == true) { - editTextEmailMsg?.append("\n" + EmailUtil.prepareReplyQuotes(msgInfo)) + editTextEmailMsg?.append("\n" + EmailUtil.genReplyContent(msgInfo)) } else { - editTextEmailMsg?.append(EmailUtil.prepareReplyQuotes(msgInfo)) + editTextEmailMsg?.append(EmailUtil.genReplyContent(msgInfo)) } editTextEmailMsg?.setSelection(currentCursorPosition) v.visibility = View.GONE @@ -1149,7 +1147,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad } editTextEmailMsg?.setText(getString(R.string.forward_template, - originalMsgInfo.getFrom()?.first()?.address ?: "", + originalMsgInfo.getFrom().first().address ?: "", EmailUtil.genForwardedMsgDate(originalMsgInfo.getReceiveDate()), originalMsgInfo.getSubject(), prepareRecipientsLineForForwarding(originalMsgInfo.getTo()))) @@ -1181,7 +1179,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad val ccSet = HashSet() if (msgInfo?.getTo()?.isNotEmpty() == true) { - for (address in msgInfo!!.getTo()!!) { + for (address in msgInfo!!.getTo()) { if (!account?.email.equals(address.address, ignoreCase = true)) { ccSet.add(address) } @@ -1201,7 +1199,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad } if (msgInfo?.getCc()?.isNotEmpty() == true) { - for (address in msgInfo!!.getCc()!!) { + for (address in msgInfo!!.getCc()) { if (!account?.email.equals(address.address, ignoreCase = true)) { ccSet.add(address) } diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/ParcelableTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/ParcelableTest.kt index ce27474109..22cf0b80ee 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/ParcelableTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/ParcelableTest.kt @@ -70,13 +70,13 @@ class ParcelableTest(val name: String, private val currentClass: Class 0).not() } .loadClasses() - .map { arrayOf(it.name, it) } + .map { arrayOf(it.name, it) } } @JvmStatic diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/EmailUtilTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/EmailUtilTest.kt index 746dea37ff..98b3731ad8 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/EmailUtilTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/EmailUtilTest.kt @@ -5,8 +5,27 @@ package com.flowcrypt.email.api.email +import com.flowcrypt.email.api.email.model.IncomingMessageInfo +import com.flowcrypt.email.api.email.model.OutgoingMessageInfo +import com.flowcrypt.email.database.entity.AccountEntity +import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.model.MessageEncryptionType +import com.flowcrypt.email.model.MessageType +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.util.* +import javax.mail.Message +import javax.mail.Multipart +import javax.mail.Session +import javax.mail.internet.InternetAddress +import javax.mail.internet.MimeMessage /** * @author Denis Bondarenko @@ -17,13 +36,117 @@ import org.junit.Test class EmailUtilTest { @Test fun testGetGmailBackupSearchQuery() { - val email = "junit@flowcrypt.com" + val email = "junit@example.com" val expectedString = """from:${email} to:${email}""" + """ (subject:"Your FlowCrypt Backup" """ + """OR subject: "Your CryptUp Backup" """ + """OR subject: "All you need to know about CryptUP (contains a backup)" """ + """OR subject: "CryptUP Account Backup") -is:spam""" val gotString = EmailUtil.getGmailBackupSearchQuery(email) - assertTrue("\nExpected:\n$expectedString\nGot:\n$gotString", expectedString == gotString) + assertEquals(expectedString, gotString) + } + + @Test + fun testGenNewMessagePlain() { + val accountEntity = AccountEntity("junit@example.com") + val subject = "subject" + val msg = "Some plain message" + val fromRecipients = listOf(InternetAddress(accountEntity.email)) + val toRecipients = listOf(InternetAddress("toRecipient@example.com")) + val ccRecipients = listOf(InternetAddress("ccRecipient@example.com")) + val bccRecipients = listOf(InternetAddress("bccRecipient@example.com")) + + val outgoingMessageInfo = OutgoingMessageInfo( + account = accountEntity.email, + subject = subject, + msg = msg, + toRecipients = toRecipients, + ccRecipients = ccRecipients, + bccRecipients = bccRecipients, + from = accountEntity.email, + encryptionType = MessageEncryptionType.STANDARD, + messageType = MessageType.NEW, + uid = 1000 + ) + val newMsg = EmailUtil.prepareNewMsg( + session = Session.getInstance(Properties()), + info = outgoingMessageInfo) + newMsg.saveChanges() + + assertEquals(subject, newMsg.subject) + assertEquals(msg, (newMsg.content as Multipart).getBodyPart(0).content) + assertEquals(fromRecipients, newMsg.from.toList()) + assertEquals(toRecipients, newMsg.getRecipients(Message.RecipientType.TO).toList()) + assertEquals(ccRecipients, newMsg.getRecipients(Message.RecipientType.CC).toList()) + assertEquals(bccRecipients, newMsg.getRecipients(Message.RecipientType.BCC).toList()) + assertEquals(LocalDate.now(), newMsg.sentDate.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate()) + assertTrue(newMsg.getHeader("MIME-Version").first() == "1.0") + } + + @Test + fun testGenReplyMessagePlain() { + val session = Session.getInstance(Properties()) + val replyToMIME = MimeMessage(session, + ByteArrayInputStream(REPLY_TO_MIME_MESSAGE.toByteArray())) + val replyToText = replyToMIME.content as String + val accountEntity = AccountEntity("receiver@receiver.com") + val receivedDate = Instant.now() + val incomingMessageInfo = IncomingMessageInfo( + msgEntity = MessageEntity( + email = accountEntity.email, + folder = "INBOX", + uid = 123, + fromAddress = InternetAddress.toString(replyToMIME.from), + receivedDate = receivedDate.toEpochMilli() + ), + text = replyToText, + encryptionType = MessageEncryptionType.STANDARD + ) + + val replyText = "Reply text" + EmailUtil.genReplyContent(incomingMessageInfo) + + val outgoingMessageInfo = OutgoingMessageInfo( + account = accountEntity.email, + //need to clarify should we override subject or not. Currently we override + subject = "subject that will be overridden", + msg = replyText, + toRecipients = replyToMIME.from.toList().map { it as InternetAddress }, + from = accountEntity.email, + encryptionType = MessageEncryptionType.STANDARD, + messageType = MessageType.REPLY, + uid = 1000 + ) + + val replyMIME = EmailUtil.genReplyMessage(replyToMsg = replyToMIME, info = outgoingMessageInfo) + replyMIME.saveChanges() + + val out = ByteArrayOutputStream() + replyMIME.writeTo(out) + println(String(out.toByteArray())) + + assertEquals("Re: " + replyToMIME.subject, replyMIME.subject) + assertEquals(replyToMIME.getRecipients(Message.RecipientType.TO).toList(), + replyMIME.from.toList()) + assertArrayEquals(replyToMIME.from, replyMIME.getRecipients(Message.RecipientType.TO)) + assertArrayEquals(replyToMIME.getHeader("Message-ID"), + replyMIME.getHeader("In-Reply-To")) + assertArrayEquals(replyToMIME.getHeader("Message-ID"), + replyMIME.getHeader("References")) + } + + companion object { + private val REPLY_TO_MIME_MESSAGE = """ + Content-Type: text/plain; charset="UTF-8" + To: receiver@receiver.com + From: sender@sender.com + Subject: Original + Date: Mon, 25 Mar 2019 14:59:11 +0000 + Message-Id: + MIME-Version: 1.0 + + orig message + """.trimIndent() } } \ No newline at end of file diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/FlowCryptMimeMessageTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/FlowCryptMimeMessageTest.kt new file mode 100644 index 0000000000..bf58b739a4 --- /dev/null +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/FlowCryptMimeMessageTest.kt @@ -0,0 +1,55 @@ +/* + * © 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 org.junit.Assert.assertEquals +import org.junit.Test +import java.util.* +import javax.mail.Session +import javax.mail.internet.InternetAddress + +/** + * @author Denis Bondarenko + * Date: 3/30/21 + * Time: 9:54 AM + * E-mail: DenBond7@gmail.com + */ +class FlowCryptMimeMessageTest { + @Test + fun testUpdateMessageIDWithFromAddress() { + val fromAddress = InternetAddress("from@example.com") + val expectedDomain = "example.com" + val addressDomain = fromAddress.domain + assertEquals(expectedDomain, addressDomain) + + val msg = FlowCryptMimeMessage(Session.getInstance(Properties())) + msg.setFrom(fromAddress) + msg.setText("Some text") + msg.saveChanges() + val messageId = msg.getHeader("Message-ID").first() + val actualDomain = extractDomainFromMessageID(messageId) + assertEquals(expectedDomain, actualDomain) + } + + @Test + fun testUpdateMessageIDWithoutFromAddress() { + val session = Session.getInstance(Properties()) + val localAddressDomain = InternetAddress.getLocalAddress(session).domain + val msg = FlowCryptMimeMessage(session) + msg.setText("Some text") + msg.saveChanges() + val messageId = msg.getHeader("Message-ID").first() + val actualDomain = extractDomainFromMessageID(messageId) + assertEquals(localAddressDomain, actualDomain) + } + + private fun extractDomainFromMessageID(messageId: String): String { + return messageId + .substring(messageId.indexOf('@') + 1) + .replace(">", "") + } +} \ No newline at end of file diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpEncryptTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpEncryptTest.kt index 240ad6678f..5978b4446e 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpEncryptTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpEncryptTest.kt @@ -35,7 +35,7 @@ class PgpEncryptTest { val sourceBytes = source.toByteArray() val outputStreamForEncryptedSource = ByteArrayOutputStream() - PgpEncrypt.encryptFile( + PgpEncrypt.encrypt( srcInputStream = ByteArrayInputStream(sourceBytes), destOutputStream = outputStreamForEncryptedSource, pgpPublicKeyRingCollection = PGPPublicKeyRingCollection(listOf(