diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 55b72cc80..2cd8f0090 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -170,6 +170,7 @@ 95473C1B297E61DE006C8957 /* SequenceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95473C1A297E61DE006C8957 /* SequenceExtension.swift */; }; 9547EF212A5F106E00A048FF /* PassPhraseAlertNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */; }; 9547EF242A5FBA2B00A048FF /* MenuSeparatorCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */; }; + 9577CEDD2AA7A4A40084AC62 /* PublicKeyDetailNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */; }; 9582BC5A2A782DA700439728 /* pass_phrase_hint.html in Resources */ = {isa = PBXBuildFile; fileRef = 9582BC592A782DA700439728 /* pass_phrase_hint.html */; }; 958566B72A6126DE001C84D3 /* EncryptedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */; }; 958566B92A612822001C84D3 /* ASButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 958566B82A612822001C84D3 /* ASButtonNode.swift */; }; @@ -628,6 +629,7 @@ 95473C1A297E61DE006C8957 /* SequenceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceExtension.swift; sourceTree = ""; }; 9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseAlertNode.swift; sourceTree = ""; }; 9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSeparatorCellNode.swift; sourceTree = ""; }; + 9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyDetailNode.swift; sourceTree = ""; }; 9582BC592A782DA700439728 /* pass_phrase_hint.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = pass_phrase_hint.html; sourceTree = ""; }; 958566B82A612822001C84D3 /* ASButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASButtonNode.swift; sourceTree = ""; }; 95E014CE2A8BF27C00D4B4F5 /* AvatarCheckboxNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCheckboxNode.swift; sourceTree = ""; }; @@ -2233,6 +2235,7 @@ 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */, 9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */, 95E014CE2A8BF27C00D4B4F5 /* AvatarCheckboxNode.swift */, + 9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */, ); path = Nodes; sourceTree = ""; @@ -2881,6 +2884,7 @@ 51B9EE6F27567B520080B2D5 /* MessageRecipientsNode.swift in Sources */, D211CE6E23FC354200D1CE38 /* CellNode.swift in Sources */, D24ABA6023FDB26C002EE9DD /* Helpers.swift in Sources */, + 9577CEDD2AA7A4A40084AC62 /* PublicKeyDetailNode.swift in Sources */, D211CE7323FC35AC00D1CE38 /* TextViewCellNode.swift in Sources */, D2A9CA392426197400E1D898 /* SigninButtonNode.swift in Sources */, D211CE7423FC35AC00D1CE38 /* ButtonCellNode.swift in Sources */, diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TableView.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TableView.swift index 9bd794e57..90e591aa4 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TableView.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TableView.swift @@ -24,16 +24,20 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { case thread, message, divider } - let attachmentsCount = input[section - 1].processedMessage?.attachments.count ?? 0 - return Parts.allCases.count + attachmentsCount + let processedMessage = input[section - 1].processedMessage + let attachmentsCount = processedMessage?.attachments.count ?? 0 + let pubkeysCount = processedMessage?.keyDetails.count ?? 0 + return Parts.allCases.count + attachmentsCount + pubkeysCount } + // swiftlint:disable cyclomatic_complexity function_body_length func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in guard let self else { return ASCellNode() } + let row = indexPath.row guard indexPath.section > 0 else { - if indexPath.row == 0 { + if row == 0 { let subject = self.inboxItem.subject ?? "no subject" return MessageSubjectNode(subject.attributed(.medium(18))) } else { @@ -44,7 +48,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { let messageIndex = indexPath.section - 1 let message = self.input[messageIndex] - if !message.rawMessage.isDraft, indexPath.row == 0 { + if !message.rawMessage.isDraft, row == 0 { return ThreadMessageInfoCellNode( input: .init(threadMessage: message, index: messageIndex), onReplyTap: { [weak self] _ in self?.handleReplyTap(at: indexPath) }, @@ -54,7 +58,7 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } if message.rawMessage.isDraft { - if indexPath.row == 0 { + if row == 0 { return self.draftNode(messageIndex: messageIndex, isExpanded: message.isExpanded) } else { return self.dividerNode(indexPath: indexPath) @@ -64,17 +68,29 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { guard message.isExpanded, let processedMessage = message.processedMessage else { return self.dividerNode(indexPath: indexPath) } - guard indexPath.row > 1 else { + guard row > 1 else { return MessageTextSubjectNode( input: .init( message: processedMessage.attributedMessage, quote: processedMessage.attributedQuote, - index: messageIndex + index: messageIndex, + isEncrypted: processedMessage.type == .encrypted ) ) } - let attachmentIndex = indexPath.row - 2 + let keyCount = processedMessage.keyDetails.count + let keyIndex = row - 2 + if keyIndex < keyCount { + let keyDetails = processedMessage.keyDetails[keyIndex] + let node = PublicKeyDetailNode(input: getPublicKeyDetailInput(for: keyDetails)) + node.onImportKey = { + self.importPublicKey(indexPath: indexPath, keyDetails: keyDetails) + } + return node + } + + let attachmentIndex = row - 2 - keyCount if let attachment = processedMessage.attachments[safe: attachmentIndex] { return AttachmentNode( input: .init( @@ -88,6 +104,8 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } } + // swiftlint:enable cyclomatic_complexity function_body_length + func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { switch tableNode.nodeForRow(at: indexPath) { case is ThreadMessageInfoCellNode: @@ -103,6 +121,13 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { } } + private func importPublicKey(indexPath: IndexPath, keyDetails: KeyDetails) { + if let email = keyDetails.pgpUserEmails.first { + try? localContactsProvider.updateKey(for: email, pubKey: .init(keyDetails: keyDetails)) + node.reloadRows(at: [indexPath], with: .automatic) + } + } + private func dividerNode(indexPath: IndexPath) -> ASCellNode { let height = indexPath.section < input.count ? 1 / UIScreen.main.nativeScale : 0 return DividerCellNode( @@ -111,6 +136,21 @@ extension ThreadDetailsViewController: ASTableDelegate, ASTableDataSource { height: height ) } + + func getPublicKeyDetailInput(for keyDetails: KeyDetails) -> PublicKeyDetailNode.Input { + let email = keyDetails.pgpUserEmails.first ?? "N/A" + let localPublicKeys = (try? localContactsProvider.retrievePubKeys(for: email, shouldUpdateLastUsed: false)) ?? [] + let importStatus: PublicKeyDetailNode.PublicKeyImportStatus = { + if localPublicKeys.contains(keyDetails.public) { return .alreadyImported } + return localPublicKeys.isNotEmpty ? .differentKeyImported : .notYetImported + }() + return PublicKeyDetailNode.Input( + email: email, + publicKey: keyDetails.public, + fingerprint: keyDetails.fingerprints.first ?? "N/A", + importStatus: importStatus + ) + } } // MARK: - Drafts diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TapActions.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TapActions.swift index e5dd45732..d48e82fd0 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TapActions.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController+TapActions.swift @@ -275,7 +275,7 @@ extension ThreadDetailsViewController { let sectionIndex = indexPath.section - 1 let section = input[sectionIndex] - let attachmentIndex = indexPath.row - 2 + let attachmentIndex = indexPath.row - 2 - (section.processedMessage?.keyDetails.count ?? 0) guard let rawAttachment = section.processedMessage?.attachments[attachmentIndex] else { throw MessageHelperError.attachmentNotFound diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 689c43efc..c78a655c8 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -32,6 +32,7 @@ final class ThreadDetailsViewController: TableNodeViewController { ) let appContext: AppContextWithUser + let localContactsProvider: LocalContactsProviderType let messageOperationsApiClient: MessageOperationsApiClient let threadOperationsApiClient: MessagesThreadOperationsApiClient var inboxItem: InboxItem @@ -58,7 +59,7 @@ final class ThreadDetailsViewController: TableNodeViewController { ) async throws { self.appContext = appContext let clientConfiguration = try await appContext.clientConfigurationProvider.configuration - let localContactsProvider = LocalContactsProvider( + self.localContactsProvider = LocalContactsProvider( encryptedStorage: appContext.encryptedStorage ) let mailProvider = try appContext.getRequiredMailProvider() diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift index 4a71c3749..862bb47fe 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageHelper.swift @@ -93,6 +93,17 @@ final class MessageHelper { ) } + func getKeyDetailsFromAttachment(attachments: inout [MessageAttachment], messageId: Identifier) async throws -> [KeyDetails] { + var keyDetailsList: [KeyDetails] = [] + for (index, attachment) in attachments.enumerated() where attachment.treatAs == "publicKey" { + let attachmentData = try await fetchOrDownloadData(for: attachment, messageId: messageId) + let parsedKeys = try await Core.shared.parseKeys(armoredOrBinary: attachmentData) + keyDetailsList += parsedKeys.keyDetails + attachments[index].data = attachmentData + } + return keyDetailsList + } + func process( message: Message, onlyLocalKeys: Bool, @@ -102,8 +113,12 @@ final class MessageHelper { var message = message try await parseAttachmentTypes(message: &message) + let keyDetails: [KeyDetails] = try await getKeyDetailsFromAttachment( + attachments: &message.attachments, + messageId: message.identifier + ) guard message.isPgp else { - return ProcessedMessage(message: message) + return ProcessedMessage(message: message, keyDetails: keyDetails) } return try await decryptAndProcess( @@ -114,6 +129,11 @@ final class MessageHelper { ) } + private func fetchOrDownloadData(for attachment: MessageAttachment, messageId: Identifier) async throws -> Data { + if let data = attachment.data { return data } + return try await download(attachment: attachment, messageId: messageId, progressHandler: nil) + } + private func getKeypairs(email: String, isUsingKeyManager: Bool) async throws -> [Keypair] { let keys = try await keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: email) @@ -247,18 +267,21 @@ final class MessageHelper { ) } - let attachments: [MessageAttachment] + var attachments: [MessageAttachment] if message.raw != nil || message.attachments.isEmpty { attachments = decrypted.blocks.compactMap(\.attMeta).compactMap(MessageAttachment.init) } else { attachments = message.attachments } + let keyDetails: [KeyDetails] = try await getKeyDetailsFromAttachment(attachments: &attachments, messageId: message.identifier) + return ProcessedMessage( message: message, text: text, type: messageType, attachments: attachments, + keyDetails: keyDetails, signature: signature ) } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift index 42b697e2d..1516cc8c4 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/ProcessedMessage.swift @@ -83,6 +83,7 @@ struct ProcessedMessage { let quote: String? let type: MessageType var attachments: [MessageAttachment] + var keyDetails: [KeyDetails] = [] var signature: MessageSignature? } @@ -92,21 +93,24 @@ extension ProcessedMessage { text: String, type: MessageType, attachments: [MessageAttachment] = [], + keyDetails: [KeyDetails] = [], signature: MessageSignature? = nil ) { self.message = message (self.text, self.quote) = Self.parseQuote(text: text) self.type = type self.attachments = attachments + self.keyDetails = keyDetails self.signature = signature } - init(message: Message) { + init(message: Message, keyDetails: [KeyDetails] = []) { self.message = message (self.text, self.quote) = Self.parseQuote(text: message.body.text) self.type = .plain self.attachments = message.attachments self.signature = .unsigned + self.keyDetails = keyDetails } } diff --git a/FlowCrypt/Functionality/Services/Local Contacts Provider/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Contacts Provider/LocalContactsProvider.swift index 30af1de30..c3d78462c 100644 --- a/FlowCrypt/Functionality/Services/Local Contacts Provider/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Contacts Provider/LocalContactsProvider.swift @@ -23,6 +23,7 @@ protocol LocalContactsProviderType: PublicKeyProvider { func searchRecipients(query: String) throws -> [Recipient] func remove(recipient: RecipientWithSortedPubKeys) throws func updateKeys(for recipient: RecipientWithSortedPubKeys) throws + func updateKey(for email: String, pubKey: PubKey) throws func getAllRecipients() async throws -> [RecipientWithSortedPubKeys] } @@ -89,6 +90,19 @@ extension LocalContactsProvider: LocalContactsProviderType { } } + func updateKey(for email: String, pubKey: PubKey) throws { + guard let recipientObject = try find(with: email) else { + try save(RecipientRealmObject(email: email, name: nil, lastUsed: nil, keys: [pubKey])) + return + } + + if let storedPubKey = recipientObject.pubKeys.first(where: { $0.primaryFingerprint == pubKey.fingerprint }) { + try update(storedPubKey: storedPubKey, newPubKey: pubKey) + } else { + try add(pubKey: pubKey, to: recipientObject) + } + } + func searchRecipient(with email: String) async throws -> RecipientWithSortedPubKeys? { guard let recipient = try find(with: email).ifNotNil(Recipient.init) else { return nil } return try await parseRecipient(from: recipient) diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index d7985e1fb..fec8d5958 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -429,3 +429,12 @@ "contacts_created" = "Created:"; "contacts_expires" = "Expires:"; "contacts_user" = "User:"; + +// Public Key Detail Node +"show_public_key" = "Show Public Key"; +"public_key_for" = "Public key for %@"; +"fingerprint_label_value" = "(Fingerprint: %@)"; +"import_public_key" = "Import Public Key"; +"update_public_key" = "Update Public Key"; +"already_imported" = "Already imported"; +"public_key_import_warning" = "Manually importing Public Keys received over email can be dangerous.\nContact the sender to verify that the fingerprint matches."; diff --git a/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift b/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift index f085e91b1..f4a1533f6 100644 --- a/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift +++ b/FlowCryptAppTests/Mocks/LocalContactsProviderMock.swift @@ -26,4 +26,6 @@ final class LocalContactsProviderMock: LocalContactsProviderType { } func removePubKey(with fingerprint: String, for email: String) {} + + func updateKey(for email: String, pubKey: FlowCrypt.PubKey) throws {} } diff --git a/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index 0e9827331..e7263171b 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -42,6 +42,12 @@ public extension String { ) } + func spaced(every n: Int) -> String { + return enumerated().reduce("") { + $0 + ($1.offset % n == 0 && $1.offset != 0 ? " " : "") + String($1.element) + } + } + func slice(from: String, to: String) -> String? { (range(of: from)?.upperBound).flatMap { substringFrom in (range(of: to, range: substringFrom ..< endIndex)?.lowerBound).map { substringTo in diff --git a/FlowCryptUI/Cell Nodes/CellNode.swift b/FlowCryptUI/Cell Nodes/CellNode.swift index ec62c033e..c3dd691ba 100644 --- a/FlowCryptUI/Cell Nodes/CellNode.swift +++ b/FlowCryptUI/Cell Nodes/CellNode.swift @@ -9,6 +9,23 @@ import AsyncDisplayKit open class CellNode: ASCellNode { + var leftBorder: ASDisplayNode? + + func addLeftBorder(width: CGFloat, color: UIColor?) { + let border = ASDisplayNode() + border.backgroundColor = color + border.style.width = ASDimension(unit: .points, value: width) + border.style.height = ASDimension(unit: .fraction, value: 1) + addSubnode(border) + leftBorder = border + } + + override public func layout() { + super.layout() + let leftBorderWidth = leftBorder?.style.width.value ?? 0 + leftBorder?.frame = CGRect(x: 0, y: 0, width: leftBorderWidth, height: self.bounds.height) + } + override public init() { super.init() automaticallyManagesSubnodes = true diff --git a/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift b/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift index 8e72dd935..30b60e228 100644 --- a/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift +++ b/FlowCryptUI/Cell Nodes/MessageTextSubjectNode.swift @@ -13,11 +13,13 @@ public final class MessageTextSubjectNode: CellNode { let message: NSAttributedString? let quote: NSAttributedString? let index: Int + let isEncrypted: Bool - public init(message: NSAttributedString?, quote: NSAttributedString?, index: Int) { + public init(message: NSAttributedString?, quote: NSAttributedString?, index: Int, isEncrypted: Bool) { self.message = message self.quote = quote self.index = index + self.isEncrypted = isEncrypted } } @@ -26,8 +28,6 @@ public final class MessageTextSubjectNode: CellNode { private let messageNode = ASEditableTextNode() private let quoteNode = ASEditableTextNode() - private let insets = UIEdgeInsets.deviceSpecificTextInsets(top: 8, bottom: 8) - private var shouldShowQuote = false private lazy var toggleQuoteButtonNode: ASButtonNode = { @@ -55,6 +55,7 @@ public final class MessageTextSubjectNode: CellNode { if let quote = input.quote { setupTextNode(quoteNode, text: quote, accessibilityIdentifier: "aid-message-\(input.index)-quote") } + addLeftBorder(width: .threadLeftBorderWidth, color: input.isEncrypted ? .main : UIColor(hex: "777777")) } private func setupTextNode(_ node: ASEditableTextNode, text: NSAttributedString?, accessibilityIdentifier: String) { @@ -95,7 +96,7 @@ public final class MessageTextSubjectNode: CellNode { } return ASInsetLayoutSpec( - insets: insets, + insets: .threadMessageInsets, child: specChild ) } diff --git a/FlowCryptUI/Cell Nodes/SwitchCellNode.swift b/FlowCryptUI/Cell Nodes/SwitchCellNode.swift index 91ae04191..fbdaae767 100644 --- a/FlowCryptUI/Cell Nodes/SwitchCellNode.swift +++ b/FlowCryptUI/Cell Nodes/SwitchCellNode.swift @@ -12,20 +12,26 @@ import AsyncDisplayKit public final class SwitchCellNode: CellNode { public struct Input { let attributedText: NSAttributedString + let accessibilityIdentifier: String? let insets: UIEdgeInsets let backgroundColor: UIColor? let isOn: Bool + let switchJustifyContent: ASStackLayoutJustifyContent public init( isOn: Bool, attributedText: NSAttributedString, + accessibilityIdentifier: String? = nil, insets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16), - backgroundColor: UIColor? = nil + backgroundColor: UIColor? = nil, + switchJustifyContent: ASStackLayoutJustifyContent = .start ) { self.attributedText = attributedText + self.accessibilityIdentifier = accessibilityIdentifier self.insets = insets self.backgroundColor = backgroundColor self.isOn = isOn + self.switchJustifyContent = switchJustifyContent } } @@ -33,6 +39,7 @@ public final class SwitchCellNode: CellNode { private lazy var switchNode = ASDisplayNode { () -> UIView in let view = UISwitch() view.isOn = self.input?.isOn ?? false + view.accessibilityIdentifier = self.input?.accessibilityIdentifier view.addTarget(self, action: #selector(self.handleAction(_:)), for: .valueChanged) return view } @@ -65,7 +72,7 @@ public final class SwitchCellNode: CellNode { return ASStackLayoutSpec( direction: .horizontal, spacing: 8, - justifyContent: .start, + justifyContent: input?.switchJustifyContent ?? .start, alignItems: .center, children: [ ASInsetLayoutSpec( diff --git a/FlowCryptUI/Nodes/PublicKeyDetailNode.swift b/FlowCryptUI/Nodes/PublicKeyDetailNode.swift new file mode 100644 index 000000000..468cb6992 --- /dev/null +++ b/FlowCryptUI/Nodes/PublicKeyDetailNode.swift @@ -0,0 +1,162 @@ +// +// PublicKeyDetailNode.swift +// FlowCryptUI +// +// Created by Ioan Moldovan on 9/5/23 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit +import UIKit + +public final class PublicKeyDetailNode: CellNode { + + public enum PublicKeyImportStatus { + case alreadyImported + case differentKeyImported + case notYetImported + } + + public struct Input { + let email: String + let publicKey: String + let fingerprint: String + let importStatus: PublicKeyImportStatus + + public init( + email: String, + publicKey: String, + fingerprint: String, + importStatus: PublicKeyImportStatus + ) { + self.email = email + self.publicKey = publicKey + self.fingerprint = fingerprint + self.importStatus = importStatus + } + } + + enum Constants { + static let fontStyle: NSAttributedString.Style = .regular(15) + } + + private lazy var publicKeyLabelNode: ASTextNode = { + let node = ASTextNode() + node.attributedText = "public_key_for" + .localizeWithArguments(input.email) + .attributed(Constants.fontStyle) + node.accessibilityIdentifier = "aid-public-key-label" + return node + }() + + private lazy var fingerprintNode: ASTextNode = { + let node = ASTextNode() + node.attributedText = "fingerprint_label_value" + .localizeWithArguments(input.fingerprint.spaced(every: 4)) + .attributed(Constants.fontStyle) + node.accessibilityIdentifier = "aid-fingerprint-value" + return node + }() + + private lazy var warningLabel: ASTextNode = { + let node = ASTextNode() + node.attributedText = "public_key_import_warning" + .localized + .attributed(Constants.fontStyle, color: .warningColor) + node.accessibilityIdentifier = "aid-warning-label" + return node + }() + + private lazy var toggleNode: SwitchCellNode = { + let input = SwitchCellNode.Input( + isOn: false, + attributedText: "show_public_key" + .localized + .attributed(Constants.fontStyle, color: .textColor), + accessibilityIdentifier: "aid-toggle-public-key-node", + backgroundColor: .clear, + switchJustifyContent: .center + ) + let node = SwitchCellNode(input: input) { isOn in + self.shouldDisplayPublicKey = isOn + } + return node + }() + + private lazy var publicKeyValueNode: ASTextNode = { + let node = ASTextNode() + node.attributedText = input.publicKey.attributed(Constants.fontStyle, color: .main) + node.accessibilityIdentifier = "aid-public-key-value" + return node + }() + + private lazy var importKeyButtonNode: ASButtonNode = { + let node = ASButtonNode() + var title: String + var isEnabled = true + var bgColor: UIColor = .warningColor + + switch input.importStatus { + case .alreadyImported: + title = "already_imported" + isEnabled = false + bgColor = .gray + case .differentKeyImported: + title = "update_public_key" + case .notYetImported: + title = "import_public_key" + } + + node.setTitle(title.localized, with: .boldSystemFont(ofSize: 16), with: .white, for: .normal) + node.accessibilityIdentifier = "aid-import-key-button" + node.addTarget(self, action: #selector(importKeyButtonTapped), forControlEvents: .touchUpInside) + + node.isEnabled = isEnabled + node.backgroundColor = bgColor + node.style.flexGrow = 1.0 + node.style.flexShrink = 1.0 + node.style.preferredSize.height = 40 + + return node + }() + + var shouldDisplayPublicKey = false { + didSet { + setNeedsLayout() + } + } + + public var onImportKey: (() -> Void)? + + let input: Input + + public init(input: Input) { + self.input = input + super.init() + backgroundColor = UIColor.colorFor(darkStyle: UIColor(hex: "303030")!, lightStyle: UIColor(hex: "FAFAFA")!) + addLeftBorder(width: .threadLeftBorderWidth, color: UIColor(hex: "989898")) + } + + @objc func importKeyButtonTapped() { + onImportKey?() + } + + override public func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + let verticalStack = ASStackLayoutSpec.vertical() + verticalStack.spacing = 10 + + verticalStack.children = [ + publicKeyLabelNode, + fingerprintNode, + input.importStatus != .alreadyImported ? warningLabel : nil, + toggleNode, + shouldDisplayPublicKey ? publicKeyValueNode : nil, + importKeyButtonNode + ].compactMap { $0 } + + return ASInsetLayoutSpec( + insets: .threadMessageInsets, + child: verticalStack + ) + } +} diff --git a/FlowCryptUI/UIConstants.swift b/FlowCryptUI/UIConstants.swift index ee37a3b46..97cf0196b 100644 --- a/FlowCryptUI/UIConstants.swift +++ b/FlowCryptUI/UIConstants.swift @@ -22,6 +22,8 @@ public extension UIEdgeInsets { let top: CGFloat = UIDevice.isIpad ? 16 : 8 return UIEdgeInsets.deviceSpecificInsets(top: top, bottom: top) } + + static var threadMessageInsets = UIEdgeInsets(top: 15, left: .Insets.textSide + .threadLeftBorderWidth, bottom: 15, right: .Insets.textSide) } public extension CGFloat { @@ -44,4 +46,6 @@ public extension CGFloat { static var width: CGFloat = 44 static var height: CGFloat = 44 } + + static var threadLeftBorderWidth: CGFloat = 4 } diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-18a8888351cc96c3.json b/appium/api-mocks/apis/google/exported-messages/message-export-18a8888351cc96c3.json new file mode 100644 index 000000000..a367ff30b --- /dev/null +++ b/appium/api-mocks/apis/google/exported-messages/message-export-18a8888351cc96c3.json @@ -0,0 +1,126 @@ +{ + "acctEmail": "e2e.enterprise.test@flowcrypt.com", + "full": { + "id": "18a8888351cc96c3", + "threadId": "18a8888351cc96c3", + "labelIds": [ + "IMPORTANT", + "CATEGORY_PERSONAL", + "INBOX" + ], + "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt Email Encryption 8.5.0 Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAgFF8gX6a7a7gna8+jZxMReo+E2fFhWysM+k97XkA", + "payload": { + "partId": "", + "mimeType": "multipart/mixed", + "filename": "", + "headers": [ + { + "name": "X-Gm-Message-State", + "value": "AOJu0YyiuZFQZiXGAbc/IznlGtk0Z/dkalgdM4h/KJQfpjsYtuuD1g/X JGeooMZLkYjeXTsg1m8m5yfrHvTv66aAzLmSYTmvh/xix4g=" + }, + { + "name": "Openpgp", + "value": "id=E8F0517BA6D7DAB6081C96E4ADAC279C95093207" + }, + { + "name": "From", + "value": "sender@domain.com" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Date", + "value": "Tue, 12 Sep 2023 01:35:40 -0700" + }, + { + "name": "Subject", + "value": "Encrypted email with public key attached" + }, + { + "name": "To", + "value": "flowcrypt.compatibility@gmail.com" + }, + { + "name": "Content-Type", + "value": "multipart/mixed; boundary=\"0000000000000524f106052554b2\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/plain; charset=\"UTF-8\"" + } + ], + "body": { + "size": 2696, + "data": "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgRW1haWwgRW5jcnlwdGlvbiA4LjUuMA0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3VjREVDJabFNtaFoxR29TQVFkQWdGRjhnWDZhN2E3Z25hOCtqWnhNUmVvK0UyZkZoV3lzTStrOTdYa0ENCjkxb3dNZVZ1QXpTdGNrUGl5T3FOdVVCSDQrdG1Wd253SlBwcGM5ZzcrYVNzSkE4RUIxUXh0bkNpWDZuTA0Kbjd2WUYyYjd3Y0ZNQTB0YUwvem1MWlVCQVEvK0pteitvbU9IR2h6OXhvM3hyeVc5a3lYUWVTcWhyMGVGDQpXK3lzcVRlbXg1VkFpOWIranB3Zlk3d3c1TkljSEErZ05pMlpUdTRhZFlRYlkvd2hTeGFQRnk3ekttQjQNCnRtMWh1ZFBsMFBZR2hlQjlJK256dUJZOE04QWNWSlBEQmhyVDVHb3E4RUpidjRUMWVtNmZJa3BNMHVjZA0KcXQwWVF2Rm1rKzVyQW9JVGhEaThncXBweUJYUnVjKy9RZWJpaXNkVVFWYjVGaDZaRDVKdXd5WExkT0ZyDQpqbE5kcnF5RndXUkt6VE5nR0tIL0ZxL3BjaXUxbEZmTnhLaWs5UHBxcThBNlh5cXU3Qmd1bENPRXZiRWINCmV3MnRUR1c5ZUw5MXJPRG1tRTd1SGxzS1lieWRMbnllVmpnMWx2ait4RjBVUVJjMzNWMmNLNUFLMC9tYw0KcTFmRGFUTHdVRTJYRVJrMWd0YllLMU11TVRFOHpnWDFvYnlPWkY0YUxjZDBjVG9ZdEM1Zy9wTWpiOWZ2DQpaUU1xa0lEbXhTRmpPa2crdHpuUm9pdlpKSlU3bDRmZG96Z2UxV1VnMnd5UVNDY01QQmFpMHViKytZZy8NCk9XSCtTNk0rY2JGVDNiNmEwTk5USkZZRmZJbnduM1puUUpRS2ZjeUdGSjM1Y2t2K2FYdVdPblNLK0UxVg0KM1FiUFI1TkZMRjJOd3NSRjA3TTV6NzM1bEdVbjUwQlhtaFJaVW5WRGRURnU0Y1YrdS80ZGRDbE1ISnpQDQo0RnJPMVh3L05BUTB6TXUwQklVVGYwaGFtUDNYNlJQRnZyVVo1bVRiR3IvQlFyMVB2dW1TTEhRcnptV2YNCkNtSnVNRUN4VEwrVFg3OVgzNHhIRkw1YUZucTBkdy9Neld1aXNGeml6cENocUpkdkNKVEJ3VXdEdmIxMg0KWVBhWmpjUUJELzlXWkZmY055ejJXMmpwZlEwZkh2ajU1Um83SXdUdFVjanhvVDR0U2RYdXViRTRCdXNpDQpZdk1Jd0EwcG9MaWFoVmxZNUh1UUNGcWIzeXFrQ2JlLzJOMnVEdGZ2RzJTc3d5WXJEamtWbVFDWVFIcysNCjBka0hIOGVweFkzWU1TdGdvdGFvWHN4RXpmK0VQTEUzaU0wNFVEUHl3WUNKZ3huenVRSmM4U1hPSTd6NA0KMWpFR3pPaG9xR29WdlYrVERibGhVNGpUdlZTYU5IdHNWcHdxaVY3enNwOVgyWmRXRzlicUtuY29KU1QvDQpzb0x3YVRQeHR3cGtWVUMwYjdRMEp2b3Y1OXFhdUxmVW9pN0VGRDBZOFdxTEFCUHJiTGdyMldMcW9xWk0NCmFQeFJ6QXk1a05Ram40b2g1MjNKYlZqQ1Y3VWE4Wi9rME1GTFl4dm0yUjQ2WVdnek8xRXE0MGpmb1Q4cA0KL2hkeUlaNzcwZmlNaXVFbXpoZVFrRnBpQnNlYmhjV3dMUk9JL0REUFM1UGN4NWE1eTMvM0hVRHdNY0lVDQpqZlZLT055ZjFjSVdyQkc0WkEzZXFQKzBieDN4TzBLVFRhaEI2eWk1OTdiMVdIMmFtdHhDa3h5a2NUUFANCnhIZnhzT01IMzNVMTd1bVFhc1VySE9QME93STVVQzQ4OTlrWGpYZlp2UGRZWGI5V3NQMUtSakliSi9nYw0Ka0ttUDh4aFc3QmlDMlM2ZEg2dzhZWldPTFhUUFBCLzhkZGs3R09zc3Rad3Axd3dNVXJFNFdyWDB2SUF4DQpxUUJpc1lqVUNkZ0c5d3JWMEVvRUhtaGRMSWc3QjNUKzlXOWpPeE1xbStid0o3QkxBcjN6K3cxWlRxdlANCmFEUENTZmhDUi90bmdWWjcyNDF6RDhHaTEwNkNVeXZ6YTlMQjdnRVB6aE1GKzBYdm9PeU8wVzYyT1RVbg0Kci9jbjRpMkkvSTJmclQ4cGprT1FPOEhMbGwzN0E3Vm52VHR6bUhkN2VBWkp1YkVqcmJZOThDNmdBRTdYDQpOTzFEa0k2d2JyWEtyUU0yREFBdWJiN2VwenYrQ3ozT3F0N3V4aW1qNldUQ25LU29GMlNvTnBrRTRjQlQNCkptUlNSNk0rVnk2N3M1RzJkLytpbkxhS2ZrN3RhSTRZa1BKTTgyQjh1MGxMUVVTb1Q1QVVtNjBPSHNOVw0KRVNhZkJnajBodmFtMTFZUk5FbWlpQVcvaXBEMDhKNks0MGdFQURhK2tJUXFiekFEaUtQemNWWlBJM2ttDQpUc2kwMmozNEpyMUtTZlJUNjF4dmtoNERUMm1nYU9KVk9JWGlsSUM0VDVVY2VOREJRcTh6Z2EzMVZZQjYNCmtFMnVEYldiVVFRY0NHNW56eGxlM1ZXNm1OL3dHRVEvc3p6TWN0YkRMVkt1c1E1Ri9yRk1MV3RXUFFJMA0KNkpBbjNuR3A3dDk2U21EcDFYS1VCMnBJc2g3dzhNekMvQTFnNks0V2VjbVQvK1R0TzUzZW81Z0duZGJsDQpWbVVYVHV5eVljNGZJSzNMbjcybk40NmgzbjE0bFBLL29waDVDTFNhTHFzZkN1Y3d3T3JaczZvQm10eHUNClE0cEVHeUx2UURBZ2xZMTllb05ZZkZmZnZQZkdXVDI5N2s3dHk1YUh5ZHUxN3Ftaml3T0lNUmhaSnBNcw0KM2xiUmdJN3RJZk9oSnliZkZBMTZsMlJIT2RDMURPZDRPR1J1aGllNG0zTTJGNm96MkNOSGhyZFNkQkZEDQozdWlPcVZ4UFZ2UVJxL2RmQUpBOXBMRHlDOGdCWDV0MjN0am1JeGNJcVJ1WnI3b3N5QmNuR2k0N0UvMWoNCmZhaGQ0YWVHWUQ0K1FpVWQ2d1ZxTzl0WHdWcHFKbUNucVdCOVczU1QwQ3NDUkM3b3cyd2I0WGl4Rmt2ZQ0KU294Q1c2ank3aHA1cXN0U1ZHNzAxNjJBTmFWcnVTaVBUYnJ3b0tSWkhFTHFheFordFRaenFwY2piVWQ5DQptTE44cEE0L3N5ekZabEhVM1UzQlROZ0RKdUhrMWllQk4zWFRwOEhvRFlkWUh1MXRyNGd5VFBhUkt3MHENCkF3TWpEMjIrVFRCQXZBVVdyaGdTZkpCTStxTXBtYW10aElvMHJRS3JXLzV2M2hxeml0cUgNCj1pdG1RDQotLS0tLUVORCBQR1AgTUVTU0FHRS0tLS0tDQo=" + } + }, + { + "partId": "1", + "mimeType": "application/pgp-keys", + "filename": "0xADAC279C95093207.asc", + "headers": [ + { + "name": "Content-Type", + "value": "application/pgp-keys; name=\"0xADAC279C95093207.asc\"" + }, + { + "name": "Content-Disposition", + "value": "attachment; filename=\"0xADAC279C95093207.asc\"" + }, + { + "name": "Content-Transfer-Encoding", + "value": "base64" + }, + { + "name": "Content-ID", + "value": "" + }, + { + "name": "X-Attachment-Id", + "value": "f_zdPmOReKOJGezzQdUVxGPXKFGWsrXL@flowcrypt" + } + ], + "body": { + "attachmentId": "ANGjdJ--zY6aZDSN86KsI4hzfoiRTK3fTOwV59LkyDVD7w_zDH8NtMWpvsfvrWVCIJe3K2Lu5iiFIZCc1HpVstvDtMvxrg3RtRTKBNlt2gaiwm2BVLrSmmry2uX4PVwunlxBvL5xDE8k8jvqi4IBdV2H0ays0giQX0V76040AFLTmCtWlbSd-dINGrDGIQv2s2h7p4EXQHg0PGKwRrplaVKVtQJD1UetU3iNBQscbyFb3CEC8trvl1ONiXku7B3VfjHlnbjPAwVZ2jgBpDdXqvI9BvBqSjskWEz6Nve2_3C9gUEIZbpvmHEVCW5MhVDTQBbOZT-c0QmLasMpNKsLoTCAt6JR5P3sTFClubolzHTXfkDz3i0e-jMleS6QzMqqP_mnbteXl7WlB68ZOTFi", + "size": 3256 + } + } + ] + }, + "sizeEstimate": 12816, + "historyId": "273528", + "internalDate": "1694507740000" + }, + "attachments": { + "ANGjdJ--zY6aZDSN86KsI4hzfoiRTK3fTOwV59LkyDVD7w_zDH8NtMWpvsfvrWVCIJe3K2Lu5iiFIZCc1HpVstvDtMvxrg3RtRTKBNlt2gaiwm2BVLrSmmry2uX4PVwunlxBvL5xDE8k8jvqi4IBdV2H0ays0giQX0V76040AFLTmCtWlbSd-dINGrDGIQv2s2h7p4EXQHg0PGKwRrplaVKVtQJD1UetU3iNBQscbyFb3CEC8trvl1ONiXku7B3VfjHlnbjPAwVZ2jgBpDdXqvI9BvBqSjskWEz6Nve2_3C9gUEIZbpvmHEVCW5MhVDTQBbOZT-c0QmLasMpNKsLoTCAt6JR5P3sTFClubolzHTXfkDz3i0e-jMleS6QzMqqP_mnbteXl7WlB68ZOTFi": { + "data": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgRW1haWwgRW5jcnlwdGlvbiA4LjQuOA0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp4c0ZOQkZuN3FWNEJFQUNnS2Z1Zkc2eXNlUlA5aktYWjF6ck01c1F0a0dXaUtMa3MxNzk5bTBLd0lZdUENClF5WXZ3NmNJV2JNMmRjdUJOT3pZSHNMcWx1cW9YYUNEYlVwSzh3SS94bkgvOVpIRHlvbWswQVNkeUkwSw0KT2duMkRyWEZ5U3VSbGdsUG1uTVFGN3ZocG5YZWZscXA5YnhROW00eWlITVMrRlFhek12Zi96Y3JBS0tnDQpoUHhjWVhDMUJKZlN1YjV0ajFyWTI0QVJwSzkxZldPUU82Z0FGVXZwZVNpTmlLYjdDNGxtV3VMZzY0VUwNCmpMVExYTzlQLzJWczJCQkhPQUNzNnUwcG1EbkZ0RG5GbGVHTEM1anJMNlZ2UURwM2VrRXZjcWNmQzVNVg0KUjBONnVWVGVzUmM1aGxCdHdoYkdnNEh1STVjRkxMK2prUndXY1ZTbHVKUzlNTXR1ZzJlVTdGQVdJek9DDQp4V2ErTGZiOGNIcEVnNmNpZEdTeFNlNDl2Z0tLcnlzdjVQZFZmT3VYaEw2M2k0VEVuS0ZzcE9ZQjhxWHkNCjVuM0ZrWUYvNUNwWU4vSFFhb0NDeERJWExHcDMzdTAzT0l0YWRBdFFVK3FBQ2FHbVJoUUE5cXdlNGkraw0KTFdMM294b1N3US9hZXdiM2ZWbytLN3lnR05sdGs2cG9IUGNMMGRVNlZIWWU4aDJNQ0VPLzFMUjd5VnNLDQpXNDdCNGZnZDNodVhoODY4QVgzWVFuNFBkNm1xZnQ0V2RjQ3VScEdKZ3ZKTkhxMThKdkl5c0RwZ3NMU3ENClFGNDRaMEdPSDJ2UXJuT2hKeElXTlVLTitRbk15OFJONlNaMVVGbzRQK3ZmMXo5N1lJMk1mck1MZkhCLw0KVFVuc3hTNmZHcktoTlZ4TjdFVEg2OXAyckk2RjgzNkVaaGViTFFBUkFRQUJ6VHRHYkc5M1EzSjVjSFFnDQpRMjl0Y0dGMGFXSnBiR2wwZVNBOFpteHZkMk55ZVhCMExtTnZiWEJoZEdsaWFXeHBkSGxBWjIxaGFXd3UNClkyOXRQc0xCZFFRUUFRZ0FLUVVDV2Z1cFl3WUxDUWNJQXdJSkVLMnNKNXlWQ1RJSEJCVUlDZ0lERmdJQg0KQWhrQkFoc0RBaDRCQUFETzVnLy9hdWRPNUU3S1hpWElRenFzVmZoMFJwT1M1S3dEYThaTkFPemJCalFiDQpmanl2am52ZWo5cFl5KzdQb3Q5TkRmR3RNRU1wV2o1dVd1UGhEMWZ2Mkt2L3VCUDRjc0pxZjhWYnMxSDENCmhENHNEMjFSckhlck03eENGeklOMVhIaGtlbVI3SUFMTmZla3JDOVRHaTRJWVlaclpLei95SzBsQ2pUOA0KQklyb2pZVUU1Q09EYThtS1BCMkJTbUp3cU53WnhocjBLS25QeWtyT0FaZnBBcm5IRWRZM0pFNTRTZTZGDQpDeEtNV090bktCSGN3SGlTVHNYL25CdEszMHNDdWw5ajFXZ2QxakZSSjI0NEVTSmQ3TTZjQmxOcko2R1QNClpEaWxybXBvOW5WTzBzbFR3RC9ZRDZHQ3lOM3IzaEozSUVEbndaSzA1cEwrMXRyTTY3MThweVdheXdmVA0KNjJ2V3pMN3BOcWs3dElnaFgrSHJ2ckhWTllzL0czTG5OOW01emxDSk1rNXdLUCtmOW9sc3ozTGx1cGFtDQoyYXVrZy9oMUhYRWwzbGxpOXU5UWtKa2JHYUVEV1I5VUNuSC94b3licFMwbWdqVll0MEI2ak5ZdkhCTEwNCmh1YWpoUisxc2pWSUlnMGt3ZnhaZlFnRlh5QUw4TFd1NG5OYVNFSUNVbDhoVkJXZjlWNlhuNFZYN0praw0KV2xFM0pFQnlZaXVaa0FEaFNkeWtsSllrUjlmUWpVYzVBY1pzVWdPdVRYc1k0ZkcwSUVyeU16cnhSdzBxDQpncUcxN3JpcjF1cXJ2TERyRE0xOEZQV2tXMkp3R3pGMFlSNXllenZ2ejNIM3JYb2crcnlFemVaQU40OFoNCndyenZHUmN2RVpKRm1CMUN3VEhyVzRVeWtDNTkycHFIUjVLNG5WN0JVVHpPd1UwRVdmdXBYZ0VRQUsvRw0KR2p5aDNDSGcweUdaTDVxNExKZm4yeEFCVjAwUlhpd3hOeVBjLzdZellnU2FuQlFtekZqM0FNSmhjRmRKDQp4L0VnM2kwcFRyNnFiQW53emtZb1NtOVI5azQwUFRBOUxQNEFNQlA0dVhpd2Jia1YyTmxvL1JNZ21ITjQNCktxdXp3WS9oYk5LNlp1akZ0REdYcDJzL3dxdGZyZm1kRG5YdVVobmlsck9vNk5SL0RydE1hRW1zWFRDZg0KUWlaam5tU2tBRUp2VlVKS2loYjlDNTFMekZTV1BZRU1rak9XbzAzWlNZSlI2Tmp1YmpNSzJoVkViaDh3DQpRN1d2dmRmc3NPaXdPK2d3WHc3emliWnBoQ01BN0FEVnFVZU0xMHEraitUTEdoL2d2cG0wZ2hxaktac2QNCmsyZWhuY1VsVFFoRGt3WThKSjVpSjZRVGhnall3YUFjQzBBa2U1ckEvN25QbjZZTW54bFAvUjdOcTY1MQ0KbDhTQm96Y1R6anNlT1N3ZWFySDV0TWVLeWFzdFRXRUlIRkFkNXJZSUVxYXdweDlGODdrTHhSaFFqOU5VDQpRNnVrbWRSNjZQOGVsc205QVpkUXVhUUY1M29FUTV6d3VVSzgrd1hxRFRDODUzWHRmSHNDdnhLRU5QMFoNClhYdnlxVm8ySU5STkJPNVdsU1lRakd4b3hvaHMxWCtDTUFtRlNEdmJWNzBkWlZmMGNRSjlHaWRvY0F2Nw0KMERPSGVYQnVPaVhaQnF5R1NOamVjUGwyYkZyNEE2cjVSTW5OWkRyWWllWEpPRVdVcWdhWDB1TlFhY1g0DQpBZWNtS2lDRXlSMDhYS0VQVm5uSkdVTTdtT3ZodUdkSDBaQzAzWlVQcUxBaGZXMmN4Y3NpT2VUUTd0YzgNCkxMYVR1MzQ4UHhWc1BrTjE5UmJCQUJFQkFBSEN3VjhFR0FFSUFCTUZBbG43cVdRSkVLMnNKNXlWQ1RJSA0KQWhzTUFBQzV1QS8rTkE0elYrTldSTklwa3lURFBEN0ZHaTRwbUZjTVVzOTZXemNlZHgyNDRhdTRpeExMDQpwclNPaWI1ZUExVUltalJXcHRKSUk2clpKQ0hWckIvckZKVlFoU0hhSlFDc1NkOEswTjFET09ydjRvYUcNCnJMOXp5elBkQVRXOGl6WTlyeklSYU5nOVNpOER2VUxmS0loZUxJNDI5UldEZmVZRmpGUFZKOG41NWd3YQ0KZjI4TnB0eHN5bzRtRVdoZitwRi9sOEhhUXRPekxCODJQRTROWHdyemYyTW9nTnozVzVCTXZjV1pvMVZtDQphNEl6MUlKZkhkTmxaWUpPMXZNQzd1LzdKWUF6dHlINTBtWFQ5Smg2VTJqaW01T0VsRlJORVVoMzVFMUwNCjJHNlh6UmRPSnJFWGJnaEY3RU8raWVrSXlSU2NmMnBFK3ZOQmhMMml3bkpzK0NoZ0ZERklHblIrWmp3bA0KM3JHOG11eDBpeWtzZTV2T1RvaWQ4U0VaMTZudTdXRjliOGhJeE9yTTdOQkFJYVdWRDlvcXN3OHUrbjMwDQpNcDBEQitwYzBNbmh5MHhqTVdkVG1MY3ArVXI1UjJ1WjZRQ1owbFl6TEZZczdaVzRYNm1UM1R3dEdXYTcNCmVCTklSaXlBQm01ZzNqaFRpOHN3UVhodjhNdEc2ZUxpeDhINS9YRE9aUzkxeTZQbFVkQWpmRFMzNC9JZQ0KTWxTOFNNMVFJbEJrTEhxSjE4dmlRTkhxdzlpWWJmNTU3TkE2QlZxbzNBMk9WUHl5Q1ZhS1JvWUgzTFRjDQpTRXB4TWNpcU9Ic3F0WWxTbzdkUnlKT0VVUTZiV0VSSUFINXZDOTVmQkxnZHF0ZWQrYTVLcS83aHg4c2YNCnJZZEwxbEp5dGlMMFZnR1dTMEdWTDFjWk1Vd2h2dnU4YnhJPQ0KPXUrLysNCi0tLS0tRU5EIFBHUCBQVUJMSUMgS0VZIEJMT0NLLS0tLS0NCg", + "size": 3256 + } + }, + "raw": { + "id": "18a8888351cc96c3", + "threadId": "18a8888351cc96c3", + "labelIds": [ + "IMPORTANT", + "CATEGORY_PERSONAL", + "INBOX" + ], + "snippet": "-----BEGIN PGP MESSAGE----- Version: FlowCrypt Email Encryption 8.5.0 Comment: Seamlessly send and receive encrypted email wV4DT2ZlSmhZ1GoSAQdAgFF8gX6a7a7gna8+jZxMReo+E2fFhWysM+k97XkA", + "sizeEstimate": 12816, + "raw": "RGVsaXZlcmVkLVRvOiBlMmUuZW50ZXJwcmlzZS50ZXN0QGZsb3djcnlwdC5jb20NClJlY2VpdmVkOiBieSAyMDAyOmEwNTo2MzU5OmJhMzpiMDoxMzk6Nzk3MjpmYzI0IHdpdGggU01UUCBpZCBnZjM1Y3NwMjQzOTQyNnJ3YjsNCiAgICAgICAgVHVlLCAxMiBTZXAgMjAyMyAwMTozNTo0MyAtMDcwMCAoUERUKQ0KWC1SZWNlaXZlZDogYnkgMjAwMjphMTc6OTBhOjEzY2E6YjA6MjY4OjI1MDA6YjE3ZSB3aXRoIFNNVFAgaWQgczEwLTIwMDIwYTE3MDkwYTEzY2EwMGIwMDI2ODI1MDBiMTdlbXI5ODI4MDkxcGpmLjIzLjE2OTQ1MDc3NDI2NjQ7DQogICAgICAgIFR1ZSwgMTIgU2VwIDIwMjMgMDE6MzU6NDIgLTA3MDAgKFBEVCkNCkFSQy1TZWFsOiBpPTE7IGE9cnNhLXNoYTI1NjsgdD0xNjk0NTA3NzQyOyBjdj1ub25lOw0KICAgICAgICBkPWdvb2dsZS5jb207IHM9YXJjLTIwMTYwODE2Ow0KICAgICAgICBiPUxlWElRdzlveFc5NFI0U0tDK3hrcFBIUzdqVkJ1bWltWnc5UHBVREZjaDhqaFN4cFpMZnROSUdlSzZjek1mU2x2UQ0KICAgICAgICAgQ21XUytMdDE0SytvNjJPUnFZRlZBRmF4ek9ndG5rNENNZzNOQXo1SGZtelpIL3ptelBGdVZhb0QwSjE2OVZhY2hvVzgNCiAgICAgICAgIHFCSW0yblo5MnNCcDlpWTFHMXlFOERrODU5Uko1Z0hnRHZFSlVrcjBtMjlUYXFtOGRoMHRVQmttaFlWOTFzRnpPVU4yDQogICAgICAgICBkMStPV01lUDVZNkNJTDFiK3VmVW0rcERMdjFzY2UyOGtGTm1IRHUyNEhtVkdPdnc2UitvQTU0RW9oaHNsdDIza0VSVQ0KICAgICAgICAgSG1mS0xBdHhGbzkzN0VTTHpPRG0yS2RhSGlUZkpCOHhVakVCUmdBdzg1UUVIVDhHQ0N6Mkl1VTZuNDRvTXM4MzFGVTUNCiAgICAgICAgIExhWEE9PQ0KQVJDLU1lc3NhZ2UtU2lnbmF0dXJlOiBpPTE7IGE9cnNhLXNoYTI1NjsgYz1yZWxheGVkL3JlbGF4ZWQ7IGQ9Z29vZ2xlLmNvbTsgcz1hcmMtMjAxNjA4MTY7DQogICAgICAgIGg9dG86c3ViamVjdDptZXNzYWdlLWlkOmRhdGU6bWltZS12ZXJzaW9uOmZyb206b3BlbnBncDpka2ltLXNpZ25hdHVyZTsNCiAgICAgICAgYmg9aGlvNzhEalVJZFBkcU4wcTBHK1dOR3ZTSW5UeExiWlVBQ2RDdWdXQ3cwUT07DQogICAgICAgIGZoPXQ1bmw2eTJPNklnVDBlL1p1ekNpTHlRcWpVSW9OZk51QXJQazVlUkxxbGc9Ow0KICAgICAgICBiPUFkQkVSNDFlWGlFQ3hiK2VEa2owd1dtcGxnTkhkNUloWTllRm9mTkNKSDRiSWJTd3ZWQ242M2c4Y2VPYmkwZWJEbA0KICAgICAgICAgQko4VHd4U0pnbUlSUkJiRXVmUnVSUVlPckZmWE5ldEwrdHhhTXI3bktMZVFjN2huS0ZETFgrVXovd0tlWWc5VUJBQUcNCiAgICAgICAgIGFyYk9VUDc4WHZCamJRbDJpVFlhMSs1QU1PNUdIWllmK3c1d3F6cUpvV01nNnVobE5rMjRVNkVBMTRYY0liTGgyUEh6DQogICAgICAgICBUbkpvd0lCQm9iVXh5YWczdGJQTFl3dTlQWW5jRzNEOGZPMThUdHR4b2MxYnplZ3NIR0p6a2FlMWsvWktZUE1QeDJ3Mg0KICAgICAgICAgZDdpWWtwT0kwR2xkQmt2TDFBOW9IMEVOYWlGRzdLTktJQlE2bU5ES0ltc0I2V09Tb0FJMGtOMU0vS0x4d1loV1ZWVUkNCiAgICAgICAgIFlraWc9PQ0KQVJDLUF1dGhlbnRpY2F0aW9uLVJlc3VsdHM6IGk9MTsgbXguZ29vZ2xlLmNvbTsNCiAgICAgICBka2ltPXBhc3MgaGVhZGVyLmk9QGdtYWlsLmNvbSBoZWFkZXIucz0yMDIyMTIwOCBoZWFkZXIuYj1lQkRCamxkKzsNCiAgICAgICBzcGY9cGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbSBkZXNpZ25hdGVzIDIwOS44NS4yMjAuNDEgYXMgcGVybWl0dGVkIHNlbmRlcikgc210cC5tYWlsZnJvbT1mbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb207DQogICAgICAgZG1hcmM9cGFzcyAocD1OT05FIHNwPVFVQVJBTlRJTkUgZGlzPU5PTkUpIGhlYWRlci5mcm9tPWdtYWlsLmNvbQ0KUmV0dXJuLVBhdGg6IDxmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20-DQpSZWNlaXZlZDogZnJvbSBtYWlsLXNvci1mNDEuZ29vZ2xlLmNvbSAobWFpbC1zb3ItZjQxLmdvb2dsZS5jb20uIFsyMDkuODUuMjIwLjQxXSkNCiAgICAgICAgYnkgbXguZ29vZ2xlLmNvbSB3aXRoIFNNVFBTIGlkIGYxMi0yMDAyMGExNzA5MGE2NjRjMDBiMDAyNzFjM2JkYjc5NHNvcjE4MzcxMzZwam0uMC4yMDIzLjA5LjEyLjAxLjM1LjQyDQogICAgICAgIGZvciA8ZTJlLmVudGVycHJpc2UudGVzdEBmbG93Y3J5cHQuY29tPg0KICAgICAgICAoR29vZ2xlIFRyYW5zcG9ydCBTZWN1cml0eSk7DQogICAgICAgIFR1ZSwgMTIgU2VwIDIwMjMgMDE6MzU6NDIgLTA3MDAgKFBEVCkNClJlY2VpdmVkLVNQRjogcGFzcyAoZ29vZ2xlLmNvbTogZG9tYWluIG9mIGZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbSBkZXNpZ25hdGVzIDIwOS44NS4yMjAuNDEgYXMgcGVybWl0dGVkIHNlbmRlcikgY2xpZW50LWlwPTIwOS44NS4yMjAuNDE7DQpBdXRoZW50aWNhdGlvbi1SZXN1bHRzOiBteC5nb29nbGUuY29tOw0KICAgICAgIGRraW09cGFzcyBoZWFkZXIuaT1AZ21haWwuY29tIGhlYWRlci5zPTIwMjIxMjA4IGhlYWRlci5iPWVCREJqbGQrOw0KICAgICAgIHNwZj1wYXNzIChnb29nbGUuY29tOiBkb21haW4gb2YgZmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tIGRlc2lnbmF0ZXMgMjA5Ljg1LjIyMC40MSBhcyBwZXJtaXR0ZWQgc2VuZGVyKSBzbXRwLm1haWxmcm9tPWZsb3djcnlwdC5jb21wYXRpYmlsaXR5QGdtYWlsLmNvbTsNCiAgICAgICBkbWFyYz1wYXNzIChwPU5PTkUgc3A9UVVBUkFOVElORSBkaXM9Tk9ORSkgaGVhZGVyLmZyb209Z21haWwuY29tDQpES0lNLVNpZ25hdHVyZTogdj0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOw0KICAgICAgICBkPWdtYWlsLmNvbTsgcz0yMDIyMTIwODsgdD0xNjk0NTA3NzQyOyB4PTE2OTUxMTI1NDI7IGRhcm49Zmxvd2NyeXB0LmNvbTsNCiAgICAgICAgaD10bzpzdWJqZWN0Om1lc3NhZ2UtaWQ6ZGF0ZTptaW1lLXZlcnNpb246ZnJvbTpvcGVucGdwOmZyb206dG86Y2MNCiAgICAgICAgIDpzdWJqZWN0OmRhdGU6bWVzc2FnZS1pZDpyZXBseS10bzsNCiAgICAgICAgYmg9aGlvNzhEalVJZFBkcU4wcTBHK1dOR3ZTSW5UeExiWlVBQ2RDdWdXQ3cwUT07DQogICAgICAgIGI9ZUJEQmpsZCtoRGs4UVV0VitOYzRRWndSSEgyNnlaN0lVamJDcHRmOHZwYlB4ZEVhRUhYSzlRWVpjeGN3Mm9VbWxrDQogICAgICAgICB1blhocjN1MjkwTmgyUXRqMjBNRjdwZnV4VmVqZTZVZzJ6blp4bm0yNXZjaW93SCtCU3F4SThtbzVlcjFlSUI4TGV3WQ0KICAgICAgICAgZ1ZUa0wrVVFaZm9zNjgrZnFyT2FOT2xvWEVFK0tnbnEyL0ZHbW9HV0E3QURMUlFnalI4YXhrTm1qS3VTZFYvR29iYkkNCiAgICAgICAgIHVUMGhJdHlHUVNHL0VrUEpNR3RZRVZYZ0NyeURYWitxeEJWSnYxekFPckpQaGduRjlSSzM3Mi9leWFhbVZ6SkR0YlBtDQogICAgICAgICBNTE9VL0g0c2owdEJSVUxrTHlpOW1wbUpqVGo1b2RPR2pHWUJGcERlUzd3QytxSlhLd1RhMWk2NXFUdmMrRU1VMll4Qg0KICAgICAgICAgR3ZKQT09DQpYLUdvb2dsZS1ES0lNLVNpZ25hdHVyZTogdj0xOyBhPXJzYS1zaGEyNTY7IGM9cmVsYXhlZC9yZWxheGVkOw0KICAgICAgICBkPTFlMTAwLm5ldDsgcz0yMDIzMDYwMTsgdD0xNjk0NTA3NzQyOyB4PTE2OTUxMTI1NDI7DQogICAgICAgIGg9dG86c3ViamVjdDptZXNzYWdlLWlkOmRhdGU6bWltZS12ZXJzaW9uOmZyb206b3BlbnBncA0KICAgICAgICAgOngtZ20tbWVzc2FnZS1zdGF0ZTpmcm9tOnRvOmNjOnN1YmplY3Q6ZGF0ZTptZXNzYWdlLWlkOnJlcGx5LXRvOw0KICAgICAgICBiaD1oaW83OERqVUlkUGRxTjBxMEcrV05HdlNJblR4TGJaVUFDZEN1Z1dDdzBRPTsNCiAgICAgICAgYj1TYzBmY2p0U1RjOEdBT1lodS9tVFNVYmY0ZUJWY0VHSjZRb3N5V1F2czNxS3ZnMXRkdnZBZERDNnlPVWxyUlk5dUENCiAgICAgICAgIHFVUHBWelBFYmhOOG1qL1BkNXJUYmRETXo5U2FRa1VhV3pOYTJXR0ZIV0JIMkZtLzcyVUZETWY4T21YS3NocTVDQnNVDQogICAgICAgICBES25jK1ppK3MrMXo1UWxBNWtKSHVoSjdiR1hJR2JlWEpqMFEyU1RTSEVKNThSUnl6cVExZUJKRHdSejMzb2htZkJ1bw0KICAgICAgICAgRmZkeGI1am94M3lncDhxeFJmdlV6dFJ6OWxzTG5TWXc3TTBkblM1VklibzlsbFJtVGx0YTVhdXZsTjNrSVRkWXd3QW4NCiAgICAgICAgIFdJbC91ZEI0eHEwbHJxalZHRC9ub3g4RTBQMEpPYklBT29XWWFtaXdtaFUyaDhjUnJDZTEySUloTG5LQ0tlUG5ZME1jDQogICAgICAgICBkMDdRPT0NClgtR20tTWVzc2FnZS1TdGF0ZTogQU9KdTBZeWl1WkZRWmlYR0FiYy9Jem5sR3RrMFovZGthbGdkTTRoL0tKUWZwanNZdHV1RDFnL1gNCglKR2Vvb01aTGtZamVYVHNnMW04bTV5ZnJIdlR2NjZhQXpMbVNZVG12aC94aXg0Zz0NClgtR29vZ2xlLVNtdHAtU291cmNlOiBBR0hUK0lHZW9CRGlxc1RpR2FLeHpacmQvczN0V01MRjhIWXFzNWdjemdNZWMvOG90cWZPM2dheVdwK1k4TTRRMVdqc1pMWElva1ZZQzJrQnNnQzI0akN5TVFRPQ0KWC1SZWNlaXZlZDogYnkgMjAwMjphMTc6OTBhOjEwMDI6YjA6MjZkOjRhZGU6ZmNmMCB3aXRoIFNNVFAgaWQNCiBiMi0yMDAyMGExNzA5MGExMDAyMDBiMDAyNmQ0YWRlZmNmMG1yMTA2NTMzNjJwamEuNC4xNjk0NTA3NzQxODA3OyBUdWUsIDEyIFNlcA0KIDIwMjMgMDE6MzU6NDEgLTA3MDAgKFBEVCkNClJlY2VpdmVkOiBmcm9tIDcxNzI4NDczMDI0NCBuYW1lZCB1bmtub3duIGJ5IGdtYWlsYXBpLmdvb2dsZS5jb20gd2l0aA0KIEhUVFBSRVNUOyBUdWUsIDEyIFNlcCAyMDIzIDAxOjM1OjQwIC0wNzAwDQpPcGVucGdwOiBpZD1FOEYwNTE3QkE2RDdEQUI2MDgxQzk2RTRBREFDMjc5Qzk1MDkzMjA3DQpGcm9tOiBGbG93Q3J5cHQgQ29tcGF0aWJpbGl0eSA8Zmxvd2NyeXB0LmNvbXBhdGliaWxpdHlAZ21haWwuY29tPg0KTUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IFR1ZSwgMTIgU2VwIDIwMjMgMDE6MzU6NDAgLTA3MDANCk1lc3NhZ2UtSUQ6IDxDQUtidUxUcGp6Mk8rR2pPR2lyVXV4RFhSUWZkLVRnZlFIT3Z3eVhmdWJMUFlyUjBCK3dAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBFbmNyeXB0ZWQgZW1haWwgd2l0aCBwdWJsaWMga2V5IGF0dGFjaGVkDQpUbzogZTJlLmVudGVycHJpc2UudGVzdEBmbG93Y3J5cHQuY29tDQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9taXhlZDsgYm91bmRhcnk9IjAwMDAwMDAwMDAwMDA1MjRmMTA2MDUyNTU0YjIiDQoNCi0tMDAwMDAwMDAwMDAwMDUyNGYxMDYwNTI1NTRiMg0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PSJVVEYtOCINCg0KLS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tDQpWZXJzaW9uOiBGbG93Q3J5cHQgRW1haWwgRW5jcnlwdGlvbiA4LjUuMA0KQ29tbWVudDogU2VhbWxlc3NseSBzZW5kIGFuZCByZWNlaXZlIGVuY3J5cHRlZCBlbWFpbA0KDQp3VjREVDJabFNtaFoxR29TQVFkQWdGRjhnWDZhN2E3Z25hOCtqWnhNUmVvK0UyZkZoV3lzTStrOTdYa0ENCjkxb3dNZVZ1QXpTdGNrUGl5T3FOdVVCSDQrdG1Wd253SlBwcGM5ZzcrYVNzSkE4RUIxUXh0bkNpWDZuTA0Kbjd2WUYyYjd3Y0ZNQTB0YUwvem1MWlVCQVEvK0pteitvbU9IR2h6OXhvM3hyeVc5a3lYUWVTcWhyMGVGDQpXK3lzcVRlbXg1VkFpOWIranB3Zlk3d3c1TkljSEErZ05pMlpUdTRhZFlRYlkvd2hTeGFQRnk3ekttQjQNCnRtMWh1ZFBsMFBZR2hlQjlJK256dUJZOE04QWNWSlBEQmhyVDVHb3E4RUpidjRUMWVtNmZJa3BNMHVjZA0KcXQwWVF2Rm1rKzVyQW9JVGhEaThncXBweUJYUnVjKy9RZWJpaXNkVVFWYjVGaDZaRDVKdXd5WExkT0ZyDQpqbE5kcnF5RndXUkt6VE5nR0tIL0ZxL3BjaXUxbEZmTnhLaWs5UHBxcThBNlh5cXU3Qmd1bENPRXZiRWINCmV3MnRUR1c5ZUw5MXJPRG1tRTd1SGxzS1lieWRMbnllVmpnMWx2ait4RjBVUVJjMzNWMmNLNUFLMC9tYw0KcTFmRGFUTHdVRTJYRVJrMWd0YllLMU11TVRFOHpnWDFvYnlPWkY0YUxjZDBjVG9ZdEM1Zy9wTWpiOWZ2DQpaUU1xa0lEbXhTRmpPa2crdHpuUm9pdlpKSlU3bDRmZG96Z2UxV1VnMnd5UVNDY01QQmFpMHViKytZZy8NCk9XSCtTNk0rY2JGVDNiNmEwTk5USkZZRmZJbnduM1puUUpRS2ZjeUdGSjM1Y2t2K2FYdVdPblNLK0UxVg0KM1FiUFI1TkZMRjJOd3NSRjA3TTV6NzM1bEdVbjUwQlhtaFJaVW5WRGRURnU0Y1YrdS80ZGRDbE1ISnpQDQo0RnJPMVh3L05BUTB6TXUwQklVVGYwaGFtUDNYNlJQRnZyVVo1bVRiR3IvQlFyMVB2dW1TTEhRcnptV2YNCkNtSnVNRUN4VEwrVFg3OVgzNHhIRkw1YUZucTBkdy9Neld1aXNGeml6cENocUpkdkNKVEJ3VXdEdmIxMg0KWVBhWmpjUUJELzlXWkZmY055ejJXMmpwZlEwZkh2ajU1Um83SXdUdFVjanhvVDR0U2RYdXViRTRCdXNpDQpZdk1Jd0EwcG9MaWFoVmxZNUh1UUNGcWIzeXFrQ2JlLzJOMnVEdGZ2RzJTc3d5WXJEamtWbVFDWVFIcysNCjBka0hIOGVweFkzWU1TdGdvdGFvWHN4RXpmK0VQTEUzaU0wNFVEUHl3WUNKZ3huenVRSmM4U1hPSTd6NA0KMWpFR3pPaG9xR29WdlYrVERibGhVNGpUdlZTYU5IdHNWcHdxaVY3enNwOVgyWmRXRzlicUtuY29KU1QvDQpzb0x3YVRQeHR3cGtWVUMwYjdRMEp2b3Y1OXFhdUxmVW9pN0VGRDBZOFdxTEFCUHJiTGdyMldMcW9xWk0NCmFQeFJ6QXk1a05Ram40b2g1MjNKYlZqQ1Y3VWE4Wi9rME1GTFl4dm0yUjQ2WVdnek8xRXE0MGpmb1Q4cA0KL2hkeUlaNzcwZmlNaXVFbXpoZVFrRnBpQnNlYmhjV3dMUk9JL0REUFM1UGN4NWE1eTMvM0hVRHdNY0lVDQpqZlZLT055ZjFjSVdyQkc0WkEzZXFQKzBieDN4TzBLVFRhaEI2eWk1OTdiMVdIMmFtdHhDa3h5a2NUUFANCnhIZnhzT01IMzNVMTd1bVFhc1VySE9QME93STVVQzQ4OTlrWGpYZlp2UGRZWGI5V3NQMUtSakliSi9nYw0Ka0ttUDh4aFc3QmlDMlM2ZEg2dzhZWldPTFhUUFBCLzhkZGs3R09zc3Rad3Axd3dNVXJFNFdyWDB2SUF4DQpxUUJpc1lqVUNkZ0c5d3JWMEVvRUhtaGRMSWc3QjNUKzlXOWpPeE1xbStid0o3QkxBcjN6K3cxWlRxdlANCmFEUENTZmhDUi90bmdWWjcyNDF6RDhHaTEwNkNVeXZ6YTlMQjdnRVB6aE1GKzBYdm9PeU8wVzYyT1RVbg0Kci9jbjRpMkkvSTJmclQ4cGprT1FPOEhMbGwzN0E3Vm52VHR6bUhkN2VBWkp1YkVqcmJZOThDNmdBRTdYDQpOTzFEa0k2d2JyWEtyUU0yREFBdWJiN2VwenYrQ3ozT3F0N3V4aW1qNldUQ25LU29GMlNvTnBrRTRjQlQNCkptUlNSNk0rVnk2N3M1RzJkLytpbkxhS2ZrN3RhSTRZa1BKTTgyQjh1MGxMUVVTb1Q1QVVtNjBPSHNOVw0KRVNhZkJnajBodmFtMTFZUk5FbWlpQVcvaXBEMDhKNks0MGdFQURhK2tJUXFiekFEaUtQemNWWlBJM2ttDQpUc2kwMmozNEpyMUtTZlJUNjF4dmtoNERUMm1nYU9KVk9JWGlsSUM0VDVVY2VOREJRcTh6Z2EzMVZZQjYNCmtFMnVEYldiVVFRY0NHNW56eGxlM1ZXNm1OL3dHRVEvc3p6TWN0YkRMVkt1c1E1Ri9yRk1MV3RXUFFJMA0KNkpBbjNuR3A3dDk2U21EcDFYS1VCMnBJc2g3dzhNekMvQTFnNks0V2VjbVQvK1R0TzUzZW81Z0duZGJsDQpWbVVYVHV5eVljNGZJSzNMbjcybk40NmgzbjE0bFBLL29waDVDTFNhTHFzZkN1Y3d3T3JaczZvQm10eHUNClE0cEVHeUx2UURBZ2xZMTllb05ZZkZmZnZQZkdXVDI5N2s3dHk1YUh5ZHUxN3Ftaml3T0lNUmhaSnBNcw0KM2xiUmdJN3RJZk9oSnliZkZBMTZsMlJIT2RDMURPZDRPR1J1aGllNG0zTTJGNm96MkNOSGhyZFNkQkZEDQozdWlPcVZ4UFZ2UVJxL2RmQUpBOXBMRHlDOGdCWDV0MjN0am1JeGNJcVJ1WnI3b3N5QmNuR2k0N0UvMWoNCmZhaGQ0YWVHWUQ0K1FpVWQ2d1ZxTzl0WHdWcHFKbUNucVdCOVczU1QwQ3NDUkM3b3cyd2I0WGl4Rmt2ZQ0KU294Q1c2ank3aHA1cXN0U1ZHNzAxNjJBTmFWcnVTaVBUYnJ3b0tSWkhFTHFheFordFRaenFwY2piVWQ5DQptTE44cEE0L3N5ekZabEhVM1UzQlROZ0RKdUhrMWllQk4zWFRwOEhvRFlkWUh1MXRyNGd5VFBhUkt3MHENCkF3TWpEMjIrVFRCQXZBVVdyaGdTZkpCTStxTXBtYW10aElvMHJRS3JXLzV2M2hxeml0cUgNCj1pdG1RDQotLS0tLUVORCBQR1AgTUVTU0FHRS0tLS0tDQoNCi0tMDAwMDAwMDAwMDAwMDUyNGYxMDYwNTI1NTRiMg0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9wZ3Ata2V5czsgbmFtZT0iMHhBREFDMjc5Qzk1MDkzMjA3LmFzYyINCkNvbnRlbnQtRGlzcG9zaXRpb246IGF0dGFjaG1lbnQ7IGZpbGVuYW1lPSIweEFEQUMyNzlDOTUwOTMyMDcuYXNjIg0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogYmFzZTY0DQpDb250ZW50LUlEOiA8Zl96ZFBtT1JlS09KR2V6elFkVVZ4R1BYS0ZHV3NyWExAZmxvd2NyeXB0Pg0KWC1BdHRhY2htZW50LUlkOiBmX3pkUG1PUmVLT0pHZXp6UWRVVnhHUFhLRkdXc3JYTEBmbG93Y3J5cHQNCg0KTFMwdExTMUNSVWRKVGlCUVIxQWdVRlZDVEVsRElFdEZXU0JDVEU5RFN5MHRMUzB0RFFwV1pYSnphVzl1T2lCR2JHOTNRM0o1Y0hRZw0KUlcxaGFXd2dSVzVqY25sd2RHbHZiaUE0TGpRdU9BMEtRMjl0YldWdWREb2dVMlZoYld4bGMzTnNlU0J6Wlc1a0lHRnVaQ0J5WldObA0KYVhabElHVnVZM0o1Y0hSbFpDQmxiV0ZwYkEwS0RRcDRjMFpPUWtadU4zRldORUpGUVVOblMyWjFaa2MyZVhObFVsQTVha3RZV2pGNg0KY2swMWMxRjBhMGRYYVV0TWEzTXhOems1YlRCTGQwbFpkVUVOQ2xGNVdYWjNObU5KVjJKTk1tUmpkVUpPVDNwWlNITk1jV3gxY1c5WQ0KWVVORVlsVndTemgzU1M5NGJrZ3ZPVnBJUkhsdmJXc3dRVk5rZVVrd1N3MEtUMmR1TWtSeVdFWjVVM1ZTYkdkc1VHMXVUVkZHTjNabw0KY0c1WVpXWnNjWEE1WW5oUk9XMDBlV2xJVFZNclJsRmhlazEyWmk5NlkzSkJTMHRuRFFwb1VIaGpXVmhETVVKS1psTjFZalYwYWpGeQ0KV1RJMFFWSndTemt4WmxkUFVVODJaMEZHVlhad1pWTnBUbWxMWWpkRE5HeHRWM1ZNWnpZMFZVd05DbXBNVkV4WVR6bFFMekpXY3pKQw0KUWtoUFFVTnpOblV3Y0cxRWJrWjBSRzVHYkdWSFRFTTFhbkpNTmxaMlVVUndNMlZyUlhaamNXTm1RelZOVmcwS1VqQk9OblZXVkdWeg0KVW1NMWFHeENkSGRvWWtkbk5FaDFTVFZqUmt4TUsycHJVbmRYWTFaVGJIVktVemxOVFhSMVp6SmxWVGRHUVZkSmVrOUREUXA0VjJFcg0KVEdaaU9HTkljRVZuTm1OcFpFZFRlRk5sTkRsMlowdExjbmx6ZGpWUVpGWm1UM1ZZYUV3Mk0yazBWRVZ1UzBaemNFOVpRamh4V0hrTg0KQ2pWdU0wWnJXVVl2TlVOd1dVNHZTRkZoYjBORGVFUkpXRXhIY0RNemRUQXpUMGwwWVdSQmRGRlZLM0ZCUTJGSGJWSm9VVUU1Y1hkbA0KTkdrcmF3MEtURmRNTTI5NGIxTjNVUzloWlhkaU0yWldieXRMTjNsblIwNXNkR3MyY0c5SVVHTk1NR1JWTmxaSVdXVTRhREpOUTBWUA0KTHpGTVVqZDVWbk5MRFFwWE5EZENOR1puWkROb2RWaG9PRFk0UVZneldWRnVORkJrTm0xeFpuUTBWMlJqUTNWU2NFZEtaM1pLVGtoeA0KTVRoS2RrbDVjMFJ3WjNOTVUzRU5DbEZHTkRSYU1FZFBTREoyVVhKdVQyaEtlRWxYVGxWTFRpdFJiazE1T0ZKT05sTmFNVlZHYnpSUQ0KSzNabU1YbzVOMWxKTWsxbWNrMU1aa2hDTHcwS1ZGVnVjM2hUTm1aSGNrdG9UbFo0VGpkRlZFZzJPWEF5Y2trMlJqZ3pOa1ZhYUdWaQ0KVEZGQlVrRlJRVUo2VkhSSFlrYzVNMUV6U2pWalNGRm5EUXBSTWpsMFkwZEdNR0ZYU25CaVIyd3daVk5CT0ZwdGVIWmtNazU1WlZoQw0KTUV4dFRuWmlXRUpvWkVkc2FXRlhlSEJrU0d4QldqSXhhR0ZYZDNVTkNsa3lPWFJRYzB4Q1pGRlJVVUZSWjBGTFVWVkRWMloxY0ZsMw0KV1V4RFVXTkpRWGRKU2tWTE1uTktOWGxXUTFSSlNFSkNWVWxEWjBsRVJtZEpRZzBLUVdoclFrRm9jMFJCYURSQ1FVRkVUelZuTHk5aA0KZFdSUE5VVTNTMWhwV0VsUmVuRnpWbVpvTUZKd1QxTTFTM2RFWVRoYVRrRlBlbUpDYWxGaURRcG1hbmwyYW01MlpXbzVjRmw1S3pkUQ0KYjNRNVRrUm1SM1JOUlUxd1YybzFkVmQxVUdoRU1XWjJNa3QyTDNWQ1VEUmpjMHB4WmpoV1luTXhTREVOQ21oRU5ITkVNakZTY2tobA0KY2swM2VFTkdla2xPTVZoSWFHdGxiVkkzU1VGTVRtWmxhM0pET1ZSSGFUUkpXVmxhY2xwTGVpOTVTekJzUTJwVU9BMEtRa2x5YjJwWg0KVlVVMVEwOUVZVGh0UzFCQ01rSlRiVXAzY1U1M1duaG9jakJMUzI1UWVXdHlUMEZhWm5CQmNtNUlSV1JaTTBwRk5UUlRaVFpHRFFwRA0KZUV0TlYwOTBia3RDU0dOM1NHbFRWSE5ZTDI1Q2RFc3pNSE5EZFd3NWFqRlhaMlF4YWtaU1NqSTBORVZUU21RM1RUWmpRbXhPY2tvMg0KUjFRTkNscEVhV3h5YlhCdk9XNVdUekJ6YkZSM1JDOVpSRFpIUTNsT00zSXphRW96U1VWRWJuZGFTekExY0V3ck1YUnlUVFkzTVRodw0KZVZkaGVYZG1WQTBLTmpKMlYzcE1OM0JPY1dzM2RFbG5hRmdyU0hKMmNraFdUbGx6TDBjelRHNU9PVzAxZW14RFNrMXJOWGRMVUN0bQ0KT1c5c2Mzb3pUR3gxY0dGdERRb3lZWFZyWnk5b01VaFlSV3d6Ykd4cE9YVTVVV3RLYTJKSFlVVkVWMUk1VlVOdVNDOTRiM2xpY0ZNdw0KYldkcVZsbDBNRUkyYWs1WmRraENURXdOQ21oMVlXcG9VaXN4YzJwV1NVbG5NR3QzWm5oYVpsRm5SbGg1UVV3NFRGZDFORzVPWVZORg0KU1VOVmJEaG9Wa0pYWmpsV05saHVORlpZTjBwcmF3MEtWMnhGTTBwRlFubFphWFZhYTBGRWFGTmtlV3RzU2xsclVqbG1VV3BWWXpWQg0KWTFwelZXZFBkVlJZYzFrMFprY3dTVVZ5ZVUxNmNuaFNkekJ4RFFwbmNVY3hOM0pwY2pGMWNYSjJURVJ5UkUweE9FWlFWMnRYTWtwMw0KUjNwR01GbFNOWGxsZW5aMmVqTklNM0pZYjJjcmNubEZlbVZhUVU0ME9Gb05DbmR5ZW5aSFVtTjJSVnBLUm0xQ01VTjNWRWh5VnpSVg0KZVd0RE5Ua3ljSEZJVWpWTE5HNVdOMEpWVkhwUGQxVXdSVmRtZFhCWVowVlJRVXN2UncwS1IycDVhRE5EU0djd2VVZGFURFZ4TkV4Sw0KWm00eWVFRkNWakF3VWxocGQzaE9lVkJqTHpkWmVsbG5VMkZ1UWxGdGVrWnFNMEZOU21oalJtUktEUXA0TDBWbk0ya3djRlJ5Tm5GaQ0KUVc1M2VtdFpiMU50T1ZJNWF6UXdVRlJCT1V4UU5FRk5RbEEwZFZocGQySmlhMVl5VG14dkwxSk5aMjFJVGpRTkNrdHhkWHAzV1M5bw0KWWs1TE5scDFha1owUkVkWWNESnpMM2R4ZEdaeVptMWtSRzVZZFZWb2JtbHNjazl2Tms1U0wwUnlkRTFoUlcxeldGUkRaZzBLVVdsYQ0KYW01dFUydEJSVXAyVmxWS1MybG9ZamxETlRGTWVrWlRWMUJaUlUxcmFrOVhiekF6V2xOWlNsSTJUbXAxWW1wTlN6Sm9Wa1ZpYURoMw0KRFFwUk4xZDJkbVJtYzNOUGFYZFBLMmQzV0hjM2VtbGlXbkJvUTAxQk4wRkVWbkZWWlUweE1IRXJhaXRVVEVkb0wyZDJjRzB3WjJoeA0KYWt0YWMyUU5DbXN5WldodVkxVnNWRkZvUkd0M1dUaEtTalZwU2paUlZHaG5hbGwzWVVGalF6QkJhMlUxY2tFdk4yNVFialpaVFc1NA0KYkZBdlVqZE9jVFkxTVEwS2JEaFRRbTk2WTFSNmFuTmxUMU4zWldGeVNEVjBUV1ZMZVdGemRGUlhSVWxJUmtGa05YSlpTVVZ4WVhkdw0KZURsR09EZHJUSGhTYUZGcU9VNVZEUXBSTm5WcmJXUlNOalpRT0dWc2MyMDVRVnBrVVhWaFVVWTFNMjlGVVRWNmQzVlZTemdyZDFoeA0KUkZSRE9EVXpXSFJtU0hORGRuaExSVTVRTUZvTkNsaFlkbmx4Vm04eVNVNVNUa0pQTlZkc1UxbFJha2Q0YjNodmFITXhXQ3REVFVGdA0KUmxORWRtSldOekJrV2xabU1HTlJTamxIYVdSdlkwRjJOdzBLTUVSUFNHVllRblZQYVZoYVFuRjVSMU5PYW1WalVHd3lZa1p5TkVFMg0KY2pWU1RXNU9Xa1J5V1dsbFdFcFBSVmRWY1dkaFdEQjFUbEZoWTFnMERRcEJaV050UzJsRFJYbFNNRGhZUzBWUVZtNXVTa2RWVFRkdA0KVDNab2RVZGtTREJhUXpBeldsVlFjVXhCYUdaWE1tTjRZM05wVDJWVVVUZDBZemdOQ2t4TVlWUjFNelE0VUhoV2MxQnJUakU1VW1KQw0KUVVKRlFrRkJTRU4zVmpoRlIwRkZTVUZDVFVaQmJHNDNjVmRSU2tWTE1uTktOWGxXUTFSSlNBMEtRV2h6VFVGQlF6VjFRUzhyVGtFMA0KZWxZclRsZFNUa2x3YTNsVVJGQkVOMFpIYVRSd2JVWmpUVlZ6T1RaWGVtTmxaSGd5TkRSaGRUUnBlRXhNRFFwd2NsTlBhV0kxWlVFeA0KVlVsdGFsSlhjSFJLU1VrMmNscEtRMGhXY2tJdmNrWktWbEZvVTBoaFNsRkRjMU5rT0Vzd1RqRkVUMDl5ZGpSdllVY05DbkpNT1hwNQ0KZWxCa1FWUlhPR2w2V1RseWVrbFNZVTVuT1ZOcE9FUjJWVXhtUzBsb1pVeEpOREk1VWxkRVptVlpSbXBHVUZaS09HNDFOV2QzWVEwSw0KWmpJNFRuQjBlSE41YnpSdFJWZG9aaXR3Umk5c09FaGhVWFJQZWt4Q09ESlFSVFJPV0hkeWVtWXlUVzluVG5velZ6VkNUWFpqVjFwdg0KTVZadERRcGhORWw2TVVsS1praGtUbXhhV1VwUE1YWk5RemQxTHpkS1dVRjZkSGxJTlRCdFdGUTVTbWcyVlRKcWFXMDFUMFZzUmxKTw0KUlZWb016VkZNVXdOQ2pKSE5saDZVbVJQU25KRldHSm5hRVkzUlU4cmFXVnJTWGxTVTJObU1uQkZLM1pPUW1oTU1tbDNia3B6SzBObw0KWjBaRVJrbEhibElyV21wM2JBMEtNM0pIT0cxMWVEQnBlV3R6WlRWMlQxUnZhV1E0VTBWYU1UWnVkVGRYUmpsaU9HaEplRTl5VFRkTw0KUWtGSllWZFdSRGx2Y1hOM09IVXJiak13RFFwTmNEQkVRaXR3WXpCTmJtaDVNSGhxVFZka1ZHMU1ZM0FyVlhJMVVqSjFXalpSUTFvdw0KYkZsNlRFWlpjemRhVnpSWU5tMVVNMVIzZEVkWFlUY05DbVZDVGtsU2FYbEJRbTAxWnpOcWFGUnBPSE4zVVZob2RqaE5kRWMyWlV4cA0KZURoSU5TOVlSRTlhVXpreGVUWlFiRlZrUVdwbVJGTXpOQzlKWlEwS1RXeFRPRk5OTVZGSmJFSnJURWh4U2pFNGRtbFJUa2h4ZHpscA0KV1dKbU5UVTNUa0UyUWxaeGJ6TkJNazlXVUhsNVExWmhTMUp2V1VnelRGUmpEUXBUUlhCNFRXTnBjVTlJYzNGMFdXeFRiemRrVW5sSw0KVDBWVlVUWmlWMFZTU1VGSU5YWkRPVFZtUWt4blpIRjBaV1FyWVRWTGNTODNhSGc0YzJZTkNuSlpaRXd4YkVwNWRHbE1NRlpuUjFkVA0KTUVkV1RERmpXazFWZDJoMmRuVTRZbmhKUFEwS1BYVXJMeXNOQ2kwdExTMHRSVTVFSUZCSFVDQlFWVUpNU1VNZ1MwVlpJRUpNVDBOTA0KTFMwdExTME5DZz09DQotLTAwMDAwMDAwMDAwMDA1MjRmMTA2MDUyNTU0YjItLQ0K", + "historyId": "273528", + "internalDate": "1694507740000" + } +} \ No newline at end of file diff --git a/appium/api-mocks/apis/google/exported-messages/message-export-18a88891abaf2322.json b/appium/api-mocks/apis/google/exported-messages/message-export-18a88891abaf2322.json new file mode 100644 index 000000000..51bb53bb7 --- /dev/null +++ b/appium/api-mocks/apis/google/exported-messages/message-export-18a88891abaf2322.json @@ -0,0 +1,110 @@ +{ + "acctEmail": "e2e.enterprise.test@flowcrypt.com", + "full": { + "id": "18a88891abaf2322", + "threadId": "18a888876a910440", + "labelIds": ["INBOX"], + "snippet": "Email with another user public key attached", + "payload": { + "partId": "", + "mimeType": "multipart/mixed", + "filename": "", + "headers": [ + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Date", + "value": "Tue, 12 Sep 2023 04:36:38 -0400" + }, + { + "name": "Subject", + "value": "Email with another user public key attached" + }, + { + "name": "From", + "value": "sender@domain.com" + }, + { + "name": "To", + "value": "e2e.enterprise.test@flowcrypt.com" + }, + { + "name": "Content-Type", + "value": "multipart/mixed; boundary=\"0000000000008ed83c060525575e\"" + } + ], + "body": { + "size": 0 + }, + "parts": [ + { + "partId": "0", + "mimeType": "text/plain", + "filename": "", + "headers": [ + { + "name": "Content-Type", + "value": "text/plain; charset=\"UTF-8\"" + } + ], + "body": { + "size": 45, + "data": "RW1haWwgd2l0aCBhbm90aGVyIHVzZXIgcHVibGljIGtleSBhdHRhY2hlZA0K" + } + }, + { + "partId": "1", + "mimeType": "application/octet-stream", + "filename": "Demo User (82BD5F7C) – Public.asc", + "headers": [ + { + "name": "Content-Type", + "value": "application/octet-stream; name=\"Demo User (82BD5F7C) – Public.asc\"" + }, + { + "name": "Content-Disposition", + "value": "attachment; filename=\"Demo User (82BD5F7C) – Public.asc\"" + }, + { + "name": "Content-Transfer-Encoding", + "value": "base64" + }, + { + "name": "X-Attachment-Id", + "value": "f_lmg26v090" + }, + { + "name": "Content-ID", + "value": "" + } + ], + "body": { + "attachmentId": "ANGjdJ9KBYYna2Iv_SpVT0qxwi6hnLRSRnWlnk3wdhdzG-O1ShrqUhrs6Zwkbv1IKiQnCzfOlGF0IsSF_L-STLFPevVwKJFJL7COk7mL4aCK5ViaNddZjUUtpFly5LkS1ZVz4JgtrTZB4leD-wIgZdLbXiv-adtmWZ545tO7ZSp9kH6sT9IqJ1ASYn8vQr0g6WdUdcJeRVbs8uTHOZYBU72ftzRKKvGITfePYZ4JHjXyZRTnxD2Yhn7OtzxzY3sqvX4npT7z-Vzkkfnem5o5NZMFp_RJ5qF7Uc41x98_bmwX8GeA3xcz1F57QkrAQWM", + "size": 3955 + } + } + ] + }, + "sizeEstimate": 6326, + "historyId": "273526", + "internalDate": "1694507798000" + }, + "attachments": { + "ANGjdJ9KBYYna2Iv_SpVT0qxwi6hnLRSRnWlnk3wdhdzG-O1ShrqUhrs6Zwkbv1IKiQnCzfOlGF0IsSF_L-STLFPevVwKJFJL7COk7mL4aCK5ViaNddZjUUtpFly5LkS1ZVz4JgtrTZB4leD-wIgZdLbXiv-adtmWZ545tO7ZSp9kH6sT9IqJ1ASYn8vQr0g6WdUdcJeRVbs8uTHOZYBU72ftzRKKvGITfePYZ4JHjXyZRTnxD2Yhn7OtzxzY3sqvX4npT7z-Vzkkfnem5o5NZMFp_RJ5qF7Uc41x98_bmwX8GeA3xcz1F57QkrAQWM": { + "data": "LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tCgptUUlOQkdUSkhyVUJFQURkWVV5elorc2c4NlJCS0ZTVmNlWUFZYTJvTGlzVGFvbFdidHlsYnhzMDJRdnFGZWFIClJRZ0tFTmdRcFNsUzRqSTh5OThsT01icmpVN0cwRkI0YlNWTSsvZU1iUFBKNzhQNDBKTG9TOFcvcXRsdEVuUVgKUG5zSzRZci95ZXJQNGZiK3h1T0JKY0hkMVoxRVdFVWFmMlIwTUZNY3ZBMUlwK2ZkcC9yM05HSVZaT2dzNWluSQpWY0ZlQmQ4M0R0cUpIeWhEQlp0K0NqYTdQMTNhbTM0V05ITWJOMWpkL0NMU2pEOFpBQUhzV0dyTzJIYjMxUFYxCld4a1EraGcxUEdia1dXTkVVa210M211a3E3MXJPYmxNN3RJTXluK2htZmo3cXF3eGJsKytyaGZGNGIxOENQUVQKLzd3d0hwVThUYXVTd252U3pxM3RvdUU2U0N2S2VLZUpwYzN2MnA3b3pCWjdwYno5bHRXTjk5K2ZuZ05kM2orTgpzZllnQVozRHgvUUFLMTdJeVRzdU9QVWtrVE80YXpkN3FGSjY2eG8rTzBmaE8yQlZOMWRGU3VaRytOYXZhMFlwCkErSHd4eDJVREdXUlZDU3dmS3lBS0EwUjZ0aTlQV1BCZmJiQis1ZWpad2d1L1RhNWd1UlNJRUlZbk5BRVY0NkYKMXZXYjl1d0N0MDVPbnpkSE5MSFRCTkMvR2pkMHNQd1BiNDlaU3V4LzljSDJkekdhMzVjN1NTVW5yQWFPT1c0QwpTM2RVYXVvVHBqNnRrQ25WaktySnNVOU5XU1R6aGtLTmRsWlk5aHR5cU9FNEFPR3hXR0pQYTN2d05zVmtjeUVhCnd2VGI0ZGI0NjI1NWhPajl2aE1hQlBlMk9SVlZUc252Mzg5b21lVUZFSy84UVNkVmpJdnB1L2FzcHdBUkFRQUIKdEI1RVpXMXZJRlZ6WlhJZ1BHUmxiVzlBWm14dmQyTnllWEIwTG1OdmJUNkpBbFFFRXdFSUFENENHd01GQ3drSQpCd0lHRlFvSkNBc0NCQllDQXdFQ0hnRUNGNEFXSVFTZzFaenhVRVZTUEdleXdlcHF3VTEyZ3IxZmZBVUNaTWtlCjBnVUpCNDlaN0FBS0NSQnF3VTEyZ3IxZmZERVZFQURGYTFKM0FwWmRvN3VCUTVoeldBSHFRQkhEaGduMnVDOVIKL1ZLVHBkNzBsYnRBaEJmOTJTd2hucWt4bTJDbEJ2NTdoUEEreXA0SWxKaFFGdEp0K0llVXNpSzBEWGNaWEpnYQpOVm5OYzNxdStGRzZDcFVrN1FMMVlCNndMWGRMMnN0ZlhpKytnR2ZBN01rUWVQZW5yUnZISGxlQ2gzRlB6bjdECmdQeW80V0dqUkR5eFlyaC9HaXByZ0tEaHh4Um9yZWNSYk4rcWlSeUJOUGVTT1k2RmFQSy90ZGx4TXRGYVlrSGoKT2ZBOWFJZHZkUTJPaHViVWdia0RDbDFGYTBUaSs2Q0JYakJ5K2dIbEtjVXpTdS9IV0RlZzFBMlBITGRqek5VLwpNdzFFbjBJQ0tGbFR6c1YxR2tDTHE1anVlbU41aVduNXFVZW5tMkFuditXKzR4R2JCcC9MQ2ZKWGltM28vWml2Clp1N3VqTHQ5endESzBkc29CQUNkd3pyTFBadWsrL1pmZVBnTDV4YmptVjlEY1g3aTVjRTUwK0dteEFpa3FRdk4KZE81SGNLZC9meGdHM3p6REhWWTZ3WTJHWUkzVjEwZlZMNlVZek1WVUJlemlnaC9FSkVHUWdTMmVybWFIeXY4RwptSGFkeFIrYVRQV0hPeURPUmpibDdtMXozSmRKTGwycVBHWHdma0RaelB5bndwRlBHVy9udlVhbTBsNVllMjE2CjFMUjhwMkVTOEdjTGNyR3BvRVJJeGVjaTRpM2hZWWYySEN5cnVKSnBIL3VoNUZ6THFGTTBic1Y2RDBJb24yeC8KQnpkQmtuYnRUUnBCNExPTkFlOWNDWUw4NFZiYW4vOXNNSDNlZmt1WFozeUV2amhwWWc2KzFlTCtyZGx1ckpHaQpTQTZZRW9rTFpJa0NWQVFUQVFnQVBoWWhCS0RWblBGUVJWSThaN0xCNm1yQlRYYUN2Vjk4QlFKa3lSNjFBaHNECkJRa0FDVHBzQlFzSkNBY0NCaFVLQ1FnTEFnUVdBZ01CQWg0QkFoZUFBQW9KRUdyQlRYYUN2Vjk4RWFZUUFKY0gKVXZ2S1ZaTzIreFFBUWRmb1ZjNnVzaGpBYkhpMkJmRTZ6SDVML3h5QVMzV2V0aHE3L2pQZy9ydHJVUTVUb1A4cQpXeHh4R1lINGhOakNJQTNnUEEzYlRGU3BkZHRhRmlMK3BJVnVrMWZQWG0xQ2xBSm00KzFvSU5zbTlURXlrUHB5Ck1iMkRLUGFFdXFlcUswUm9nNExOVmM4OUJES0hTUURVdURIejR1ZmcwT3BsR2ViTi83TFNUMjBJeFg4WVBWQXcKT0luT2ZhK0p3Y3ZLSGFFeFpmNTFOdXRYTndGNmVPcW5IUVYyM1lIb1dldVBGMEx2ZnZvYk4wV3JXU2QwMnd5dQpKRGRuWWZiWDlaUjVLZ0RkdStCR3c2ZkJSUFNzTUhrSnpyUnczTTIvU1NKL3ZNOEs5ay94M2xKOXpUQTREaWU4Cm5TWGkvVFpkeDZPU2ZXNWlTa1hNdFFQclZ2aTFKdXllNVZKNlhnZU82bHZDeWk3K01naXlUR2VZT1RUa2ZBZDEKZCtmQkkzNHpOQmdoajB2UEd0WE9IVmUxTzRNb2pwUUF2cnh3eU9FOWNOMlhuY0F6RXpTTTRpMVpHK1hDWmdxVgpBbzNQcE8zeUJlS1RielBvR3pHS1RTVGM5YklYMk41MmNzUG1EQlV4b2JTcUJhV01TR0wrbXlaWk9kUzl5Q2VZCmlNTFlDY3FsUFVBMlJkMkdKeTAzSGswc3RpN3Y0T2xpTEt3M0djYkp3dzB3QndMeVpBVUJVWE1ncGFFZElGenMKbDBmeEhzanI2R0cwVlFsUGxnYXpYdzlpOGZCK1NrdjR4ZkpsS1ZsQWVaeEQ0WDNDSklwZ2ZhY0ZnZThGUWlVTwpjR0UzWUNnd01uZFlqaEpSTEJIeEl5NnU4ME0yOThEZGR1b0RaSzVkdVFJTkJHVEpIclVCRUFEWVJ5NE1teEgzCkFaV0ZHZ0trdDJSdXk2MVJZcXFoKzlmQkl1U0F4Y0IyeVQrT2pZMFh6VS9yVXEzTDJSbWpZRWZ6YkpvZ2JYSU0KbnJvNXJ5M2RXUVU1cU4yYTIzQkt0aE9lV2R6YW5EYWs4aUsrVjlzYkhKNzlUZ1MyaWVOb0xWb05Ucm1YZ3dqeQpIZUVqYVM0TXFpcW1BQit1cVdUN1FyOWNTbGZsQ3NmN1lzL21ONHQxTHAwOXoxd2FDSjVCaW4rSzVUKzZIaVVvCkpIeHp5ZEV1cXV1cXBlaWhsUldrbExUaEhYRTFRcjJ2SGF0YnR0Q3NFc28wenI1R1BCTnFiT0J6TWYzbXRPaTAKcWJyb2NFWnVmc2FveFlxZk52VzhCUXFkTlNhVXA3bEFXZ2tsVkdibkQ4OC9Ud2V0OEkxdEVvUXVYeE5lV2NLNQpJRUFpaFc3bjFMMm4yVVMwRmtlL3lxWTd2MDA4T3NqQ0Z5bWVmMCtCQzdnOUVPMCtyMEtvbjMreVFBRW44WDhYCnRkRTZrRXl3dkIxQXgyaG1KTTA3cG5nZFVhdmgrS0Q3eSttYmhzY2Q3OTEzck4rVnZyMThkRGtxeXB5bmhORHEKSzFNb2hzY0kxTlhDUEJMeWlpMDRtUUJIN2U4RHlnL29ZN1ZURzI3STZxVWZ4Y09LOEgvazMwUG9xejVJZFV6ZgpxYVc2b05DeTd2ZVVjc3d2U0lDU0dJblBjcTdzVmhKWWRTVWZ1UmsvYzJuMm1nL2ZrcE5MS1E1dm45bmsxTXRPCjk0dXovZktuaWJjaENQWjRtV3NkT3JTek9RUDErRUE5aUErUkszV21UUEF5ZU1PVHBtOEhQYTR0cEFkT0wrR3UKN3BqQVJUQW5rckllTVB6Mm0xcDI1Z3Nkb2ZLWXg1UWpUd0FSQVFBQmlRSThCQmdCQ0FBbUZpRUVvTldjOFZCRgpVanhuc3NIcWFzRk5kb0s5WDN3RkFtVEpIclVDR3d3RkNRQUpPbXdBQ2drUWFzRk5kb0s5WDN3VFF3Ly9mZzhlCjVXVW5YWTJqaUhlaUJSSC9aS2xVc0NxK0lFS3E5THplRWNuSFpPVXIrbE9JMWlibHBxQWNlQ09SREl2UnU1Z3EKZXh3QXpxdkRuWStjaXZDTHdoUDhJbGFwWjR6cFl0VXk5YWYxSFNyZWMwOXk2czVxSVRjcjNPdmM3NTJhakZVOApGVUpHQ2ZZMFdxdEdZSk5WQnZLYVJ2MDlWM2Y0eWU5M0YvTHEyT3ZOUCtPc1dPdnI2T2drc2FaT2dJR0NFVm1QCkdZSXQxSW5PZnM3aEk0c082WGQ3dkF6aHhkMFhXU1Q5WHpxdm9EeGc5SVFROStxQ0duM3NINjFCNHlqRXdPd0wKS1JhUHlLVHhzOG1aNVUxekowME1sMFM1UjliV3RuT1ZnSGdmQzBZS1NEckUraVc5VnJKdTh1RXl0Qm8vdmxMQgpUZWdNazNBb2srTll6cnprRXpRVHU0cU8vNmt4N291M0NqSkVKWjBKck1xeFM5U3ZDV1hWZGFKTzNaRDBWYjZ6Cm1FYWQ3NVh1cXNvYUxubXlVZm05L0R1VTdTUnpHd0RWK0l2bUNNZHBUd3cxc2gzamNuZUEyVkM3bUVkNjc3Y2MKZjNQcW5iS2xnM2VrSXF2d0NEMHcxWHhvNDQ3R0tJcW9XTFhGYzBjRzBwc0tsZ2hlYzk1RnZPbkJWakt3K0g1TApTMEZlV2EwOGg3elB2NzJXeVhRUlc1UTZaQXd1ZDJNVzh1TVZaMEdNSWVlQWdPckNCeXB6eXArWXZHZVhLREZJCkxwTEZtTk5qQk9xOG1ENWRDbWQ2QnpjZGZLa0xFaG9wZ1V3UjBaQllNR0FJK05tbGdzTkpUYUJmcGJXdU5qemIKclptc01ZeDJqaGhadEdQaFZtU3YxOUMxOFdLUnF6c1pIeUhDZHJNPQo9NTNxLwotLS0tLUVORCBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tCg", + "size": 3955 + } + }, + "raw": { + "id": "18a88891abaf2322", + "threadId": "18a888876a910440", + "labelIds": ["INBOX"], + "snippet": "Email with another user public key attached", + "sizeEstimate": 6326, + "raw": "TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IFR1ZSwgMTIgU2VwIDIwMjMgMDQ6MzY6MzggLTA0MDANCk1lc3NhZ2UtSUQ6IDxDQUdLa0pVYThnelJMYjM4c0RodW1TSFVDMEU9bzFPVzhWNVhFd3ZDQWstbTZLLU04amdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBFbWFpbCB3aXRoIGFub3RoZXIgdXNlciBwdWJsaWMga2V5IGF0dGFjaGVkDQpGcm9tOiBlMmUgZW50ZXJwcmlzZSB0ZXN0IGF0IEZsb3dDcnlwdCA8ZTJlLmVudGVycHJpc2UudGVzdEBmbG93Y3J5cHQuY29tPg0KVG86IEZsb3dDcnlwdCBDb21wYXRpYmlsaXR5IDxmbG93Y3J5cHQuY29tcGF0aWJpbGl0eUBnbWFpbC5jb20-DQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9taXhlZDsgYm91bmRhcnk9IjAwMDAwMDAwMDAwMDhlZDgzYzA2MDUyNTU3NWUiDQoNCi0tMDAwMDAwMDAwMDAwOGVkODNjMDYwNTI1NTc1ZQ0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PSJVVEYtOCINCg0KRW1haWwgd2l0aCBhbm90aGVyIHVzZXIgcHVibGljIGtleSBhdHRhY2hlZA0KDQotLTAwMDAwMDAwMDAwMDhlZDgzYzA2MDUyNTU3NWUNCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtOyANCgluYW1lPSI9P1VURi04P1E_RGVtb19Vc2VyXz0yODgyQkQ1RjdDPTI5Xz1FMj04MD05M19QdWJsaWM9MkVhc2M_PSINCkNvbnRlbnQtRGlzcG9zaXRpb246IGF0dGFjaG1lbnQ7IA0KCWZpbGVuYW1lPSI9P1VURi04P1E_RGVtb19Vc2VyXz0yODgyQkQ1RjdDPTI5Xz1FMj04MD05M19QdWJsaWM9MkVhc2M_PSINCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NA0KWC1BdHRhY2htZW50LUlkOiBmX2xtZzI2djA5MA0KQ29udGVudC1JRDogPGZfbG1nMjZ2MDkwPg0KDQpMUzB0TFMxQ1JVZEpUaUJRUjFBZ1VGVkNURWxESUV0RldTQkNURTlEU3kwdExTMHRDZ3B0VVVsT1FrZFVTa2h5VlVKRlFVUmtXVlY1DQplbG9yYzJjNE5sSkNTMFpUVm1ObFdVRlpZVEp2VEdselZHRnZiRmRpZEhsc1luaHpNREpSZG5GR1pXRklDbEpSWjB0RlRtZFJjRk5zDQpVelJxU1RoNU9UaHNUMDFpY21wVk4wY3dSa0kwWWxOV1RTc3ZaVTFpVUZCS056aFFOREJLVEc5VE9GY3ZjWFJzZEVWdVVWZ0tVRzV6DQpTelJaY2k5NVpYSlFOR1ppSzNoMVQwSktZMGhrTVZveFJWZEZWV0ZtTWxJd1RVWk5ZM1pCTVVsd0syWmtjQzl5TTA1SFNWWmFUMmR6DQpOV2x1U1FwV1kwWmxRbVE0TTBSMGNVcEllV2hFUWxwMEswTnFZVGRRTVROaGJUTTBWMDVJVFdKT01XcGtMME5NVTJwRU9GcEJRVWh6DQpWMGR5VHpKSVlqTXhVRll4Q2xkNGExRXJhR2N4VUVkaWExZFhUa1ZWYTIxME0yMTFhM0UzTVhKUFlteE5OM1JKVFhsdUsyaHRabW8zDQpjWEYzZUdKc0t5dHlhR1pHTkdJeE9FTlFVVlFLTHpkM2QwaHdWVGhVWVhWVGQyNTJVM3B4TTNSdmRVVTJVME4yUzJWTFpVcHdZek4yDQpNbkEzYjNwQ1dqZHdZbm81YkhSWFRqazVLMlp1WjA1a00yb3JUZ3B6WmxsblFWb3pSSGd2VVVGTE1UZEplVlJ6ZFU5UVZXdHJWRTgwDQpZWHBrTjNGR1NqWTJlRzhyVHpCbWFFOHlRbFpPTVdSR1UzVmFSeXRPWVhaaE1GbHdDa0VyU0hkNGVESlZSRWRYVWxaRFUzZG1TM2xCDQpTMEV3VWpaMGFUbFFWMUJDWm1KaVFpczFaV3BhZDJkMUwxUmhOV2QxVWxOSlJVbFpiazVCUlZZME5rWUtNWFpYWWpsMWQwTjBNRFZQDQpibnBrU0U1TVNGUkNUa012UjJwa01ITlFkMUJpTkRsYVUzVjRMemxqU0RKa2VrZGhNelZqTjFOVFZXNXlRV0ZQVDFjMFF3cFRNMlJWDQpZWFZ2VkhCcU5uUnJRMjVXYWt0eVNuTlZPVTVYVTFSNmFHdExUbVJzV2xrNWFIUjVjVTlGTkVGUFIzaFhSMHBRWVROMmQwNXpWbXRqDQplVVZoQ25kMlZHSTBaR0kwTmpJMU5XaFBhamwyYUUxaFFsQmxNazlTVmxaVWMyNTJNemc1YjIxbFZVWkZTeTg0VVZOa1ZtcEpkbkIxDQpMMkZ6Y0hkQlVrRlJRVUlLZEVJMVJWcFhNWFpKUmxaNldsaEpaMUJIVW14aVZ6bEJXbTE0ZG1ReVRubGxXRUl3VEcxT2RtSlVOa3BCDQpiRkZGUlhkRlNVRkVORU5IZDAxR1EzZHJTUXBDZDBsSFJsRnZTa05CYzBOQ1FsbERRWGRGUTBoblJVTkdORUZYU1ZGVFp6RmFlbmhWDQpSVlpUVUVkbGVYZGxjSEYzVlRFeVozSXhabVpCVlVOYVRXdGxDakJuVlVwQ05EbGFOMEZCUzBOU1FuRjNWVEV5WjNJeFptWkVSVlpGDQpRVVJHWVRGS00wRndXbVJ2TjNWQ1VUVm9lbGRCU0hGUlFraEVhR2R1TW5WRE9WSUtMMVpMVkhCa056QnNZblJCYUVKbU9USlRkMmh1DQpjV3Q0YlRKRGJFSjJOVGRvVUVFcmVYQTBTV3hLYUZGR2RFcDBLMGxsVlhOcFN6QkVXR05hV0VwbllRcE9WbTVPWXpOeGRTdEdSelpEDQpjRlZyTjFGTU1WbENObmRNV0dSTU1uTjBabGhwS3l0blIyWkJOMDFyVVdWUVpXNXlVblpJU0d4bFEyZ3pSbEI2YmpkRUNtZFFlVzgwDQpWMGRxVWtSNWVGbHlhQzlIYVhCeVowdEVhSGg0VW05eVpXTlNZazRyY1dsU2VVSk9VR1ZUVDFrMlJtRlFTeTkwWkd4NFRYUkdZVmxyDQpTR29LVDJaQk9XRkpaSFprVVRKUGFIVmlWV2RpYTBSRGJERkdZVEJVYVNzMlEwSllha0o1SzJkSWJFdGpWWHBUZFM5SVYwUmxaekZCDQpNbEJJVEdScWVrNVZMd3BOZHpGRmJqQkpRMHRHYkZSNmMxWXhSMnREVEhFMWFuVmxiVTQxYVZkdU5YRlZaVzV0TWtGdWRpdFhLelI0DQpSMkpDY0M5TVEyWktXR2x0TTI4dldtbDJDbHAxTjNWcVRIUTVlbmRFU3pCa2MyOUNRVU5rZDNweVRGQmFkV3NyTDFwbVpWQm5URFY0DQpZbXB0VmpsRVkxZzNhVFZqUlRVd0swZHRlRUZwYTNGUmRrNEtaRTgxU0dOTFpDOW1lR2RITTNwNlJFaFdXVFozV1RKSFdVa3pWakV3DQpabFpNTmxWWmVrMVdWVUpsZW1sbmFDOUZTa1ZIVVdkVE1tVnliV0ZJZVhZNFJ3cHRTR0ZrZUZJcllWUlFWMGhQZVVSUFVtcGliRGR0DQpNWG96U21SS1RHd3ljVkJIV0hkbWEwUmFlbEI1Ym5kd1JsQkhWeTl1ZGxWaGJUQnNOVmxsTWpFMkNqRk1Vamh3TWtWVE9FZGpUR055DQpSM0J2UlZKSmVHVmphVFJwTTJoWldXWXlTRU41Y25WS1NuQklMM1ZvTlVaNlRIRkdUVEJpYzFZMlJEQkpiMjR5ZUM4S1FucGtRbXR1DQpZblJVVW5CQ05FeFBUa0ZsT1dORFdVdzRORlppWVc0dk9YTk5TRE5sWm10MVdGb3plVVYyYW1od1dXYzJLekZsVEN0eVpHeDFja3BIDQphUXBUUVRaWlJXOXJURnBKYTBOV1FWRlVRVkZuUVZCb1dXaENTMFJXYmxCR1VWSldTVGhhTjB4Q05tMXlRbFJZWVVOMlZqazRRbEZLDQphM2xTTmpGQmFITkVDa0pSYTBGRFZIQnpRbEZ6U2tOQlkwTkNhRlZMUTFGblRFRm5VVmRCWjAxQ1FXZzBRa0ZvWlVGQlFXOUtSVWR5DQpRbFJZWVVOMlZqazRSV0ZaVVVGS1kwZ0tWWFoyUzFaYVR6SXJlRkZCVVdSbWIxWmpOblZ6YUdwQllraHBNa0ptUlRaNlNEVk1MM2g1DQpRVk16VjJWMGFIRTNMMnBRWnk5eWRISlZVVFZVYjFBNGNRcFhlSGg0UjFsSU5HaE9ha05KUVROblVFRXpZbFJHVTNCa1pIUmhSbWxNDQpLM0JKVm5Wck1XWlFXRzB4UTJ4QlNtMDBLekZ2U1U1emJUbFVSWGxyVUhCNUNrMWlNa1JMVUdGRmRYRmxjVXN3VW05bk5FeE9WbU00DQpPVUpFUzBoVFVVUlZkVVJJZWpSMVptY3dUM0JzUjJWaVRpODNURk5VTWpCSmVGZzRXVkJXUVhjS1QwbHVUMlpoSzBwM1kzWkxTR0ZGDQplRnBtTlRGT2RYUllUbmRHTm1WUGNXNUlVVll5TTFsSWIxZGxkVkJHTUV4MlpuWnZZazR3VjNKWFUyUXdNbmQ1ZFFwS1JHUnVXV1ppDQpXRGxhVWpWTFowUmtkU3RDUjNjMlprSlNVRk56VFVoclNucHlVbmN6VFRJdlUxTktMM1pOT0VzNWF5OTRNMnhLT1hwVVFUUkVhV1U0DQpDbTVUV0drdlZGcGtlRFpQVTJaWE5XbFRhMWhOZEZGUWNsWjJhVEZLZFhsbE5WWktObGhuWlU4MmJIWkRlV2szSzAxbmFYbFVSMlZaDQpUMVJVYTJaQlpERUtaQ3RtUWtrek5IcE9RbWRvYWpCMlVFZDBXRTlJVm1VeFR6Uk5iMnB3VVVGMmNuaDNlVTlGT1dOT01saHVZMEY2DQpSWHBUVFRScE1WcEhLMWhEV21keFZncEJiek5RY0U4emVVSmxTMVJpZWxCdlIzcEhTMVJUVkdNNVlrbFlNazQxTW1OelVHMUVRbFY0DQpiMkpUY1VKaFYwMVRSMHdyYlhsYVdrOWtVemw1UTJWWkNtbE5URmxEWTNGc1VGVkJNbEprTWtkS2VUQXpTR3N3YzNScE4zWTBUMnhwDQpURXQzTTBkallrcDNkekIzUW5kTWVWcEJWVUpWV0UxbmNHRkZaRWxHZW5NS2JEQm1lRWh6YW5JMlIwY3dWbEZzVUd4bllYcFlkemxwDQpPR1pDSzFOcmRqUjRaa3BzUzFac1FXVmFlRVEwV0RORFNrbHdaMlpoWTBablpUaEdVV2xWVHdwalIwVXpXVU5uZDAxdVpGbHFhRXBTDQpURUpJZUVsNU5uVTRNRTB5T1RoRVpHUjFiMFJhU3pWa2RWRkpUa0pIVkVwSWNsVkNSVUZFV1ZKNU5FMXRlRWd6Q2tGYVYwWkhaMHRyDQpkREpTZFhrMk1WSlpjWEZvS3psbVFrbDFVMEY0WTBJeWVWUXJUMnBaTUZoNlZTOXlWWEV6VERKU2JXcFpSV1o2WWtwdloySllTVTBLDQpibkp2TlhKNU0yUlhVVlUxY1U0eVlUSXpRa3QwYUU5bFYyUjZZVzVFWVdzNGFVc3JWamx6WWtoS056bFVaMU15YVdWT2IweFdiMDVVDQpjbTFZWjNkcWVRcElaVVZxWVZNMFRYRnBjVzFCUWl0MWNWZFVOMUZ5T1dOVGJHWnNRM05tTjFsekwyMU9OSFF4VEhBd09Yb3hkMkZEDQpTalZDYVc0clN6VlVLelpJYVZWdkNrcEllSHA1WkVWMWNYVjFjWEJsYVdoc1VsZHJiRXhVYUVoWVJURlJjakoyU0dGMFluUjBRM05GDQpjMjh3ZW5JMVIxQkNUbkZpVDBKNlRXWXpiWFJQYVRBS2NXSnliMk5GV25WbWMyRnZlRmx4Wms1MlZ6aENVWEZrVGxOaFZYQTNiRUZYDQpaMnRzVmtkaWJrUTRPQzlVZDJWME9Fa3hkRVZ2VVhWWWVFNWxWMk5MTlFwSlJVRnBhRmMzYmpGTU1tNHlWVk13Um10bEwzbHhXVGQyDQpNREE0VDNOcVEwWjViV1ZtTUN0Q1F6ZG5PVVZQTUN0eU1FdHZiak1yZVZGQlJXNDRXRGhZQ25Sa1JUWnJSWGwzZGtJeFFYZ3lhRzFLDQpUVEEzY0c1blpGVmhkbWdyUzBRM2VTdHRZbWh6WTJRM09URXpjazRyVm5aeU1UaGtSR3R4ZVhCNWJtaE9SSEVLU3pGTmIyaHpZMGt4DQpUbGhEVUVKTWVXbHBNRFJ0VVVKSU4yVTRSSGxuTDI5Wk4xWlVSekkzU1RaeFZXWjRZMDlMT0VndmF6TXdVRzl4ZWpWSlpGVjZaZ3B4DQpZVmMyYjA1RGVUZDJaVlZqYzNkMlUwbERVMGRKYmxCamNUZHpWbWhLV1dSVFZXWjFVbXN2WXpKdU1tMW5MMlpyY0U1TVMxRTFkbTQ1DQpibXN4VFhSUENqazBkWG92Wmt0dWFXSmphRU5RV2pSdFYzTmtUM0pUZWs5UlVERXJSVUU1YVVFclVrc3pWMjFVVUVGNVpVMVBWSEJ0DQpPRWhRWVRSMGNFRmtUMHdyUjNVS04zQnFRVkpVUVc1cmNrbGxUVkI2TW0weGNESTFaM05rYjJaTFdYZzFVV3BVZDBGU1FWRkJRbWxSDQpTVGhDUW1kQ1EwRkJiVVpwUlVWdlRsZGpPRlpDUmdwVmFuaHVjM05JY1dGelJrNWtiMHM1V0ROM1JrRnRWRXBJY2xWRFIzZDNSa05SDQpRVXBQYlhkQlEyZHJVV0Z6Ums1a2IwczVXRE4zVkZGM0x5OW1aemhsQ2pWWFZXNVlXVEpxYVVobGFVSlNTQzlhUzJ4VmMwTnhLMGxGDQpTM0U1VEhwbFJXTnVTRnBQVlhJcmJFOUpNV2xpYkhCeFFXTmxRMDlTUkVsMlVuVTFaM0VLWlhoM1FYcHhka1J1V1N0amFYWkRUSGRvDQpVRGhKYkdGd1dqUjZjRmwwVlhrNVlXWXhTRk55WldNd09YazJjelZ4U1ZSamNqTlBkbU0zTlRKaGFrWlZPQXBHVlVwSFEyWlpNRmR4DQpkRWRaU2s1V1FuWkxZVkoyTURsV00yWTBlV1U1TTBZdlRIRXlUM1pPVUN0UGMxZFBkbkkyVDJkcmMyRmFUMmRKUjBORlZtMVFDa2RaDQpTWFF4U1c1UFpuTTNhRWswYzA4MldHUTNka0Y2YUhoa01GaFhVMVE1V0hweGRtOUVlR2M1U1ZGUk9TdHhRMGR1TTNOSU5qRkNOSGxxDQpSWGRQZDB3S1MxSmhVSGxMVkhoek9HMWFOVlV4ZWtvd01FMXNNRk0xVWpsaVYzUnVUMVpuU0dkbVF6QlpTMU5FY2tVcmFWYzVWbkpLDQpkVGgxUlhsMFFtOHZkbXhNUWdwVVpXZE5hek5CYjJzclRsbDZjbnByUlhwUlZIVTBjVTh2Tm10NE4yOTFNME5xU2tWS1dqQktjazF4DQplRk01VTNaRFYxaFdaR0ZLVHpOYVJEQldZalo2Q20xRllXUTNOVmgxY1hOdllVeHViWGxWWm0wNUwwUjFWVGRUVW5wSGQwUldLMGwyDQpiVU5OWkhCVWQzY3hjMmd6YW1OdVpVRXlWa00zYlVWa05qYzNZMk1LWmpOUWNXNWlTMnhuTTJWclNYRjJkME5FTUhjeFdIaHZORFEzDQpSMHRKY1c5WFRGaEdZekJqUnpCd2MwdHNaMmhsWXprMVJuWlBia0pXYWt0M0swZzFUQXBUTUVabFYyRXdPR2czZWxCMk56SlhlVmhSDQpVbGMxVVRaYVFYZDFaREpOVnpoMVRWWmFNRWROU1dWbFFXZFBja05DZVhCNmVYQXJXWFpIWlZoTFJFWkpDa3h3VEVadFRrNXFRazl4DQpPRzFFTldSRGJXUTJRbnBqWkdaTGEweEZhRzl3WjFWM1VqQmFRbGxOUjBGSkswNXRiR2R6VGtwVVlVSm1jR0pYZFU1cWVtSUtjbHB0DQpjMDFaZURKcWFHaGFkRWRRYUZadFUzWXhPVU14T0ZkTFVuRjZjMXBJZVVoRFpISk5QUW85TlROeEx3b3RMUzB0TFVWT1JDQlFSMUFnDQpVRlZDVEVsRElFdEZXU0JDVEU5RFN5MHRMUzB0Q2c9PQ0KLS0wMDAwMDAwMDAwMDA4ZWQ4M2MwNjA1MjU1NzVlLS0=", + "historyId": "273526", + "internalDate": "1694507798000" + } +} diff --git a/appium/api-mocks/apis/google/google-messages.ts b/appium/api-mocks/apis/google/google-messages.ts index 6b0f5bdf1..23c33301f 100644 --- a/appium/api-mocks/apis/google/google-messages.ts +++ b/appium/api-mocks/apis/google/google-messages.ts @@ -27,4 +27,6 @@ export type GoogleMockMessage = | 'rich text message with empty body and attachment' | 'message with kdbx file' | 'mime message with large attachment' - | 'Test forward message with attached pub key'; + | 'Test forward message with attached pub key' + | 'Encrypted email with public key attached' + | 'Email with another user public key attached'; diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 614d70e6b..e12af6707 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -97,6 +97,16 @@ export const CommonData = { attachmentName: 'simple.txt', encryptedAttachmentName: 'simple.txt.pgp', }, + encryptedEmailWithPublicKey: { + subject: 'Encrypted email with public key attached', + publicKeyEmail: 'flowcrypt.compatibility@gmail.com', + publicKeyFingerPrint: 'E8F0 517B A6D7 DAB6 081C 96E4 ADAC 279C 9509 3207', + }, + emailWithAnotherUserPublicKey: { + subject: 'Email with another user public key attached', + publicKeyEmail: 'demo@flowcrypt.com', + publicKeyFingerPrint: 'A0D5 9CF1 5045 523C 67B2 C1EA 6AC1 4D76 82BD 5F7C', + }, encryptedEmailWithAttachmentWithoutPreview: { sender: 'flowcrypt.compatibility@gmail.com', subject: 'message with kdbx file', diff --git a/appium/tests/helpers/ElementHelper.ts b/appium/tests/helpers/ElementHelper.ts index d21b1455f..d03479cad 100644 --- a/appium/tests/helpers/ElementHelper.ts +++ b/appium/tests/helpers/ElementHelper.ts @@ -109,11 +109,20 @@ class ElementHelper { }; //wait for text in element during 15 seconds (if the text doesn't appear during 15s, it will show the error) - static waitForText = async (element: WebdriverIO.Element, text: string, timeout: number = DEFAULT_TIMEOUT) => { + static waitForText = async ( + element: WebdriverIO.Element, + text: string, + timeout: number = DEFAULT_TIMEOUT, + checkContains = false, + ) => { await this.waitElementVisible(element); await element.waitUntil( async function () { - return (await element.getText()) === text; + const elementText = await element.getText(); + if (checkContains) { + return elementText.includes(text); + } + return elementText === text; }, { timeout: timeout, diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index d1b9db56c..1c9e187b8 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -30,6 +30,12 @@ const SELECTORS = { ENCRYPTION_BADGE: '~aid-encryption-badge', SIGNATURE_BADGE: '~aid-signature-badge', ATTACHMENT_TEXT_VIEW: '~aid-attachment-text-view', + PUBLIC_KEY_LABEL: '~aid-public-key-label', + FINGEPRINT_LABEL_VALUE: '~aid-fingerprint-value', + PUBLIC_KEY_IMPORT_WARNING: '~aid-warning-label', + TOGGLE_PUBLIC_KEY_NODE: '~aid-toggle-public-key-node', + PUBLIC_KEY_VALUE: '~aid-public-key-value', + IMPORT_PUBLIC_KEY_BUTTON: '~aid-import-key-button', }; class EmailScreen extends BaseScreen { @@ -137,6 +143,30 @@ class EmailScreen extends BaseScreen { return $(SELECTORS.SIGNATURE_BADGE); } + get publicKeyLabel() { + return $(SELECTORS.PUBLIC_KEY_LABEL); + } + + get fingerprintLabelValue() { + return $(SELECTORS.FINGEPRINT_LABEL_VALUE); + } + + get publicKeyImportWarningLabel() { + return $(SELECTORS.PUBLIC_KEY_IMPORT_WARNING); + } + + get publicKeyToggle() { + return $(SELECTORS.TOGGLE_PUBLIC_KEY_NODE); + } + + get publicKeyValueLabel() { + return $(SELECTORS.PUBLIC_KEY_VALUE); + } + + get importPublicKeyButton() { + return $(SELECTORS.IMPORT_PUBLIC_KEY_BUTTON); + } + get attachmentTextView() { return $(SELECTORS.ATTACHMENT_TEXT_VIEW); } @@ -329,6 +359,26 @@ class EmailScreen extends BaseScreen { expect(text.includes(value)).toBeTruthy(); }; + checkPublicKeyImportView = async (email: string, fingerprint: string, isAlreadyImported = false) => { + await ElementHelper.waitForText(await this.publicKeyLabel, email, 3000, true); + await ElementHelper.waitForText(await this.fingerprintLabelValue, fingerprint, 3000, true); + await ElementHelper.waitElementVisible(await this.importPublicKeyButton); + if (!isAlreadyImported) { + await ElementHelper.waitElementVisible(await this.publicKeyImportWarningLabel); + } + // Check if public key toggle works correctly (should show/hide public key value label) + await ElementHelper.waitAndClick(await this.publicKeyToggle); + await ElementHelper.waitElementVisible(await this.publicKeyValueLabel); + await ElementHelper.waitAndClick(await this.publicKeyToggle); + await ElementHelper.waitElementInvisible(await this.publicKeyValueLabel); + }; + + importPublicKey = async () => { + await ElementHelper.waitAndClick(await this.importPublicKeyButton); + await ElementHelper.waitForText(await this.importPublicKeyButton, 'Already imported'); + await ElementHelper.waitElementInvisible(await this.publicKeyImportWarningLabel); + }; + draftBody = async (index: number) => { return $(`~aid-draft-body-${index}`); }; diff --git a/appium/tests/specs/mock/inbox/ImportPublicKeyReceivedByEmail.spec.ts b/appium/tests/specs/mock/inbox/ImportPublicKeyReceivedByEmail.spec.ts new file mode 100644 index 000000000..b8b0b47e7 --- /dev/null +++ b/appium/tests/specs/mock/inbox/ImportPublicKeyReceivedByEmail.spec.ts @@ -0,0 +1,66 @@ +import { + ContactScreen, + EmailScreen, + MailFolderScreen, + MenuBarScreen, + SettingsScreen, + SetupKeyScreen, + SplashScreen, +} from '../../../screenobjects/all-screens'; + +import { MockApi } from 'api-mocks/mock'; +import { MockApiConfig } from 'api-mocks/mock-config'; +import { MockUserList } from 'api-mocks/mock-data'; +import { CommonData } from '../../../data'; + +describe('INBOX: ', () => { + it('user is able to import public key received by email', async () => { + const emailSubject = CommonData.encryptedEmailWithPublicKey.subject; + const publicKeyEmail = CommonData.encryptedEmailWithPublicKey.publicKeyEmail; + const publicKeyFingerPrint = CommonData.encryptedEmailWithPublicKey.publicKeyFingerPrint; + const emailSubject2 = CommonData.emailWithAnotherUserPublicKey.subject; + const publicKeyEmail2 = CommonData.emailWithAnotherUserPublicKey.publicKeyEmail; + const publicKeyFingerPrint2 = CommonData.emailWithAnotherUserPublicKey.publicKeyFingerPrint; + const mockApi = new MockApi(); + + mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration; + mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration; + mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com', { + messages: ['Encrypted email with public key attached', 'Email with another user public key attached'], + }); + mockApi.attesterConfig = { + servedPubkeys: { + [MockUserList.e2e.email]: MockUserList.e2e.pub!, + }, + }; + + await mockApi.withMockedApis(async () => { + await SplashScreen.mockLogin(); + await SetupKeyScreen.setPassPhrase(); + await MailFolderScreen.checkInboxScreen(); + + await MailFolderScreen.clickOnEmailBySubject(emailSubject); + await EmailScreen.checkPublicKeyImportView(publicKeyEmail, publicKeyFingerPrint, false); + await EmailScreen.importPublicKey(); + await EmailScreen.clickBackButton(); + + // Go to Contacts screen and see if pubkey is imported correctly + await MenuBarScreen.clickMenuBtn(); + await MenuBarScreen.clickSettingsButton(); + await SettingsScreen.clickOnSettingItem('Contacts'); + await ContactScreen.checkContact(publicKeyEmail); + + // Now go back to inbox screen and check `email with another user pubkey attached` + await ContactScreen.clickBackButton(); + await MenuBarScreen.clickMenuBtn(); + await MenuBarScreen.clickInboxButton(); + await MailFolderScreen.clickOnEmailBySubject(emailSubject2); + await EmailScreen.checkPublicKeyImportView(publicKeyEmail2, publicKeyFingerPrint2, false); + await EmailScreen.importPublicKey(); + // Check if import button changed to `Already imported` after public key is imported + await EmailScreen.clickBackButton(); + await MailFolderScreen.clickOnEmailBySubject(emailSubject2); + await EmailScreen.checkPublicKeyImportView(publicKeyEmail2, publicKeyFingerPrint2, true); + }); + }); +});