Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2421 feat: added ability to message recipient without valid public key #2429

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,11 @@ extension ComposeViewController {
}

switch error {
case MessageValidationError.noPubRecipients:
guard isMessagePasswordSupported else {
showPlainMessageConfirmationAlert()
return
}

if Bundle.isEnterprise {
setMessagePassword()
} else {
showPlainMessageAlert()
}
case MessageValidationError.noPubRecipients,
MessageValidationError.revokedKeyRecipients,
MessageValidationError.expiredKeyRecipients,
MessageValidationError.notUsableForEncryptionKeyRecipients:
processSendMessageWithNoValidKeys(error: error)
case MessageValidationError.notUniquePassword,
MessageValidationError.subjectContainsPassword,
MessageValidationError.weakPassword:
Expand All @@ -86,26 +80,39 @@ extension ComposeViewController {
}
}

private func showPlainMessageConfirmationAlert() {
showAlertWithAction(
private func processSendMessageWithNoValidKeys(error: Error) {
let alert = UIAlertController(
title: "compose_message_encryption".localized,
message: "compose_plain_message_confirmation".localized,
actionButtonTitle: "compose_send_unencrypted".localized,
actionAccessibilityIdentifier: "aid-compose-send-plain",
onAction: { [weak self] _ in self?.handleSendTap(shouldSendPlainMessage: true) }
message: error.errorMessage,
preferredStyle: .alert
)
}

private func showPlainMessageAlert() {
showAlertWithAction(
title: "compose_message_encryption".localized,
message: "compose_plain_message_alert".localized,
cancelButtonTitle: "compose_add_message_password".localized,
actionButtonTitle: "compose_send_unencrypted".localized,
actionAccessibilityIdentifier: "aid-compose-send-plain",
onAction: { [weak self] _ in self?.handleSendTap(shouldSendPlainMessage: true) },
onCancel: { [weak self] _ in self?.setMessagePassword() }
let sendUnEncryptedAction = UIAlertAction(
title: "compose_send_unencrypted".localized,
style: .default,
handler: { [weak self] _ in
self?.handleSendTap(shouldSendPlainMessage: true)
}
)
sendUnEncryptedAction.accessibilityIdentifier = "aid-compose-send-plain"
let sendPasswordProtectedAction = UIAlertAction(
title: "compose_add_message_password".localized,
style: .default,
handler: { [weak self] _ in
self?.setMessagePassword()
}
)
sendPasswordProtectedAction.accessibilityIdentifier = "aid-compose-send-message-password"
let cancelAction = UIAlertAction(
title: "cancel".localized,
style: .cancel
)
cancelAction.accessibilityIdentifier = "aid-cancel-button"
alert.addAction(sendUnEncryptedAction)
ioanmo226 marked this conversation as resolved.
Show resolved Hide resolved
if isMessagePasswordSupported {
alert.addAction(sendPasswordProtectedAction)
}
alert.addAction(cancelAction)
present(alert, animated: true, completion: nil)
}

private func reEnableSendButton() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource {
case (.main, .recipients(.cc)), (.main, .recipients(.bcc)):
return !shouldShowEmailRecipientsLabel && shouldShowAllRecipientTypes ? 1 : 0
case (.main, .password):
return isMessagePasswordSupported && contextToSend.hasRecipientsWithoutPubKey ? 1 : 0
return isMessagePasswordSupported && contextToSend.hasRecipientWithInvalidKey ? 1 : 0
ioanmo226 marked this conversation as resolved.
Show resolved Hide resolved
case (.main, .compose):
return ComposePart.allCases.count
case (.main, .attachments):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ extension ComposeMessageContext {
recipients.contains(where: { $0.keyState == .active })
}

var hasRecipientWithInvalidKey: Bool {
recipients.contains(where: { $0.keyState != .active })
}

var hasRecipientsWithoutPubKey: Bool {
recipients.contains(where: { $0.keyState == .empty })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,13 @@ final class ComposeMessageHelper {
throw MessageValidationError.noPubRecipients
}

guard !contains(keyState: .notUsableForEncryption) else {
guard hasMessagePassword || !contains(keyState: .notUsableForEncryption) else {
throw MessageValidationError.notUsableForEncryptionKeyRecipients
}
guard !contains(keyState: .expired) else {
guard hasMessagePassword || !contains(keyState: .expired) else {
throw MessageValidationError.expiredKeyRecipients
}
guard !contains(keyState: .revoked) else {
guard hasMessagePassword || !contains(keyState: .revoked) else {
throw MessageValidationError.revokedKeyRecipients
}
}
Expand Down
2 changes: 1 addition & 1 deletion FlowCrypt/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"compose_sign_passphrase_required" = "Passphrase is required for message signing.";
"compose_sign_passphrase_no_match" = "This pass phrase did not match your signing private key.";
"compose_sign_no_keys" = "Cannot sign message: none of your %@ account keys can be used for sending address %@";
"compose_password_placeholder" = "Tap to add password for recipients who don't have encryption set up.";
"compose_password_placeholder" = "Tap to add password for recipients without encryption or those with an invalid public key.";
"compose_password_set_message" = "Web portal password added";
"compose_password_modal_title" = "Set web portal password";
"compose_password_modal_message" = "The recipients will receive a link to read your message on a web portal, where they will need to enter this password.\n\nYou are responsible for sharing this password with recipients (use other medium to share the password - not email)\n\nPassword should include:\n- one uppercase\n- one lowercase\n- one number\n- one special character eg &/#\"-'_%-@,;:!*()\n- min 8 characters length";
Expand Down
35 changes: 5 additions & 30 deletions appium/tests/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ export const CommonData = {
password: 'abcABC1*',
modalMessage: `Set web portal password\nThe recipients will receive a link to read your message on a web portal, where they will need to enter this password.\n\nYou are responsible for sharing this password with recipients (use other medium to share the password - not email)\n\nPassword should include: - one uppercase - one lowercase - one number - one special character eg &/#"-'_%-@,;:!*() - min 8 characters length`,
plainMessageModal:
"Message Encryption\n One or more of your recipients don't have encryption set up.\n\nPlease add a message password, or message will be sent unencrypted.",
emptyPasswordMessage: "Tap to add password for recipients who don't have encryption set up.",
'Message Encryption\nOne or more of your recipients are missing a public key (marked in gray).\n\nPlease ask them to share it with you, or ask them to also set up FlowCrypt.',
emptyPasswordMessage: 'Tap to add password for recipients without encryption or those with an invalid public key.',
addedPasswordMessage: 'Web portal password added',
weakPasswordMessage:
"Error\nPassword didn't comply with company policy, which requires at least:\n\n- one uppercase - one lowercase - one number - one special character eg &/#\"-'_%-@,;:!*() - 8 characters length\n\nPlease update the password and re-send.",
Expand All @@ -220,36 +220,11 @@ export const CommonData = {
subject: 'Honor reply-to address - plain',
},
errors: {
noPublicKey:
'Error\n' +
'Could not compose message\n' +
'\n' +
'One or more of your recipients are missing a public key (marked in gray).\n' +
'\n' +
'Please ask them to share it with you, or ask them to also set up FlowCrypt.',
wrongPassPhrase:
'Error\n' + 'Could not compose message\n' + '\n' + 'This pass phrase did not match your signing private key.',
notUsableEncryptionPublicKey:
'Error\n' +
'Could not compose message\n' +
'\n' +
'One or more of your recipients have sign-only public keys (marked in yellow).\n' +
'\n' +
'Please ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.',
expiredPublicKey:
'Error\n' +
'Could not compose message\n' +
'\n' +
'One or more of your recipients have expired public keys (marked in orange).\n' +
'\n' +
'Please ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.',
revokedPublicKey:
'Error\n' +
'Could not compose message\n' +
'\n' +
'One or more of your recipients have revoked public keys (marked in red).\n' +
'\n' +
'Please ask them to send you a new public key. If this is an enterprise installation, please ask your systems admin.',
notUsableEncryptionPublicKey: `Message Encryption\nOne or more of your recipients have sign-only public keys (marked in yellow).\n\nPlease ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.`,
expiredPublicKey: `Message Encryption\nOne or more of your recipients have expired public keys (marked in orange).\n\nPlease ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.`,
revokedPublicKey: `Message Encryption\nOne or more of your recipients have revoked public keys (marked in red).\n\nPlease ask them to send you a new public key. If this is an enterprise installation, please ask your systems admin.`,
wrongPassPhraseOnLogin: 'Error\n' + 'Wrong pass phrase, please try again',
attachmentDecryptKeyMismatchError:
'Error decrypting attachment\n' +
Expand Down
8 changes: 6 additions & 2 deletions appium/tests/helpers/ElementHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,13 @@ class ElementHelper {
}

await this.waitAndClick(element);
await this.waitAndClick(element);

await browser.pause(200);
const selectAllButton = await $('~Select All');
// Check if 'Select All' is not displayed, if so, click the element again.
if (!(await selectAllButton.isDisplayed())) {
await this.waitAndClick(element);
}

await this.waitAndClick(selectAllButton);

await driver.sendKeys(['\b']); // backspace
Expand Down
6 changes: 5 additions & 1 deletion appium/tests/helpers/TouchHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ class TouchHelper {
pressOptions = { x: width, y: height };
break;
}
await TouchHelper.tapScreenAt(pressOptions);
};

static tapScreenAt = async ({ x, y }: { x: number; y: number }) => {
await driver.touchPerform([
{ action: 'press', options: pressOptions },
{ action: 'press', options: { x, y } },
{ action: 'wait', options: { ms: 100 } },
{ action: 'release', options: {} },
]);
Expand Down
17 changes: 11 additions & 6 deletions appium/tests/screenobjects/attachment.screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import ElementHelper from '../helpers/ElementHelper';

const SELECTORS = {
BACK_BTN: '~aid-back-button',
SYSTEM_BACK_BTN: '~Back',
SAVE_BTN: '~aid-save-attachment-to-device',
CANCEL_BTN: '~Cancel', // can't change aid for UIDocumentPickerViewController
};
Expand All @@ -17,10 +16,6 @@ class AttachmentScreen extends BaseScreen {
return $(SELECTORS.BACK_BTN);
}

get systemBackButton() {
return $(SELECTORS.SYSTEM_BACK_BTN);
}

get saveButton() {
return $(SELECTORS.SAVE_BTN);
}
Expand All @@ -42,7 +37,17 @@ class AttachmentScreen extends BaseScreen {
clickSystemBackButton = async () => {
// Due to a issue in SemaphoreCI environment, a single back button click does not yield the expected behavior.
// Therefore, we have implemented a mechanism to continuously click the system back button until cancel button appears.
await ElementHelper.clickUntilExpectedElementAppears(await this.systemBackButton, await this.cancelButton, 10);
await browser.pause(1000);
let systemBackButton = await $('~Back');
if (!(await systemBackButton.isDisplayed())) {
const browseButton = await $('~Browse'); // Back button is renamed to Browse in newer iOS versions
if (await browseButton.isDisplayed()) {
systemBackButton = browseButton;
} else {
throw new Error('System backup button is not displayed either using Back or Browse selector');
}
}
await ElementHelper.clickUntilExpectedElementAppears(systemBackButton, await this.cancelButton, 10);
};

clickCancelButton = async () => {
Expand Down
4 changes: 4 additions & 0 deletions appium/tests/screenobjects/new-message.screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ class NewMessageScreen extends BaseScreen {
setComposeSecurityMessage = async (message: string) => {
await browser.pause(500);
const el = await this.composeSecurityMessage;
// click message field so that message field is focused
// https://github.com/FlowCrypt/flowcrypt-ios/pull/2429#discussion_r1389511041
const location = await el.getLocation();
await TouchHelper.tapScreenAt({ x: location.x + 5, y: location.y + 5 });
await ElementHelper.clearInput(el);
await ElementHelper.waitClickAndType(el, message);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('COMPOSE EMAIL: ', () => {
await NewMessageScreen.composeEmail(recipient.email, subject, message);
await NewMessageScreen.clickSendButton();
await BaseScreen.checkModalMessage(notUsableEncryptionPublicKeyError);
await BaseScreen.clickOkButtonOnError();
await BaseScreen.clickCancelButton();

// Stage2: Now try to encrypt & send message for user which contains sign only key & normal key and check if message is sent correctly
mockApi.attesterConfig = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('COMPOSE EMAIL: ', () => {

await BaseScreen.checkModalMessage(expiredPublicKeyError);

await BaseScreen.clickOkButtonOnError();
await BaseScreen.clickCancelButton();
await NewMessageScreen.clickBackButton();
await MailFolderScreen.checkInboxScreen();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import BaseScreen from '../../../screenobjects/base.screen';
import { MockApi } from 'api-mocks/mock';
import { MockApiConfig } from 'api-mocks/mock-config';
import { MockUserList } from 'api-mocks/mock-data';
import AppiumHelper from 'tests/helpers/AppiumHelper';

describe('COMPOSE EMAIL: ', () => {
it('sending message to user without public key produces password modal', async () => {
Expand All @@ -24,13 +23,10 @@ describe('COMPOSE EMAIL: ', () => {
const emailPassword = CommonData.recipientWithoutPublicKey.password;

const plainMessageModal = CommonData.recipientWithoutPublicKey.plainMessageModal;
const passwordModalMessage = CommonData.recipientWithoutPublicKey.modalMessage;
const emptyPasswordMessage = CommonData.recipientWithoutPublicKey.emptyPasswordMessage;
const subjectPasswordErrorMessage = CommonData.recipientWithoutPublicKey.subjectPasswordErrorMessage;
const addedPasswordMessage = CommonData.recipientWithoutPublicKey.addedPasswordMessage;

const enterpriseProcessArgs = [...CommonData.mockProcessArgs, ...['--enterprise']];

const mockApi = new MockApi();

mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration;
Expand Down Expand Up @@ -58,8 +54,6 @@ describe('COMPOSE EMAIL: ', () => {
await BaseScreen.checkModalMessage(plainMessageModal);
await NewMessageScreen.clickCancelButton();

await BaseScreen.checkModalMessage(passwordModalMessage);
await NewMessageScreen.clickCancelButton();
await NewMessageScreen.checkPasswordCell(emptyPasswordMessage);

await NewMessageScreen.deleteAddedRecipient(0);
Expand All @@ -68,8 +62,6 @@ describe('COMPOSE EMAIL: ', () => {
await NewMessageScreen.clickSendButton();
await BaseScreen.checkModalMessage(plainMessageModal);
await NewMessageScreen.clickCancelButton();
await BaseScreen.checkModalMessage(passwordModalMessage);
await NewMessageScreen.clickCancelButton();
await NewMessageScreen.checkPasswordCell(emptyPasswordMessage);

// check message password validation
Expand Down Expand Up @@ -99,20 +91,6 @@ describe('COMPOSE EMAIL: ', () => {

await EmailScreen.checkOpenedEmail(sender, emailSubject, emailText);
await EmailScreen.checkEncryptionBadge('not encrypted');

// check if enterprise doesn't allow to send non-encrypted message
await AppiumHelper.restartApp(enterpriseProcessArgs);

await MailFolderScreen.checkInboxScreen();
await MailFolderScreen.clickCreateEmail();
await NewMessageScreen.composeEmail(recipient, emailSubject, emailText);
await NewMessageScreen.checkFilledComposeEmailInfo({
recipients: [recipient],
subject: emailSubject,
message: emailText,
});
await NewMessageScreen.clickSendButton();
await BaseScreen.checkModalMessage(passwordModalMessage);
ioanmo226 marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
Loading