-
Notifications
You must be signed in to change notification settings - Fork 11
Closed
Labels
Milestone
Description
When sending password protected message, we must upload it to FES & format the Gmail message differently. You can study EncryptedMsgMailFormatter on browser extension for details not mentioned here.
The steps are (when sending a password-protected message through FES:
- get a message token (reply token) from FES
- encode a string that contains sender, recipient, subject, reply token, as follows:
replyInfo = Str.base64urlUtfEncode(JSON.stringify({sender: "...", recipient: ["..."], subject: "...", token: "..."})) - format a div html element that contains .
infoDiv = '<div style="display: none" class="cryptup_reply" cryptup-data="REPLY INFO STRING"></div>' - append the div to plaintext as follows:
bodyWithReplyToken = newMsgData.plaintext + '\n\n' + infoDiv - construct a regular plain mime message using the above plain text + attachments:
pgpMimeWithAttachments = Mime.encode(bodyWithReplyToken, { Subject: newMsg.subject }, attachments); - encrypt the encoded plain mime message ONLY FOR THE MESSAGE PASSWORD, no public keys.
pwdEncryptedWithAttachments = encryptDataArmor(pgpMimeWithAttachments, newMsg.pwd, []); // encrypted only for pwd, not signed. This message will be uploaded, therefore it's only encrypted for message password. - upload resulting data to FES.
msgUrl = this.view.acctServer.messageUpload(idToken, pwdEncryptedWithAttachments, replyToken, from, recipients, { renderUploadProgress(p, PercentageAccounting.FIRST_HALF) } ). Notice that the upload progress will be divided by two, so that instead of rendering 0-100%, we render 0-50% at this stage. - encrypt and format the final Gmail message. First encrypt body:
encryptedTextFile = Core.encryptFile(newMsgData.plaintext, publicKeys) // no attachments, no infoDiv, no mime. Use public keys to encrypt as usual, as available. No password - then encrypt attachments
encryptedAttachments = plainAttachments.map { Core.encryptFile($0, publicKeys) }// add.pgpto name if not done automatically - put all attachments into an array
encryptedAttachmentsAndBodyAsFiles = [Attachment(data=encryptedTextFile, name=message.asc, mimeType=application/pgp-encrypted)] + encryptedAttachments - format message text to go into the email, which is in plain text. See method
formatPwdEncryptedMsgBodyLink.emailAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(msgUrl);- NOTE: skip theintropart from the linked code, we are not implementing this on mobile yet - now all data is encrypted manually. The password-encrypted one was uploaded to FES and we have URL, which was encoded into
emailAndLinkBody. Attachments were encrypted separately. Compose it all into a plain message together:sendableMessageMime = Core.composeEmail(format=plain, emailAndLinkBody, encryptedAttachmentsAndBodyAsFiles) - finally you have mime data to send. Upload to gmail & use
PercentageAccounting.SECOND_HALF, eg on browser extension (simplified):
// if this is a password-encrypted message, then we've already shown progress for uploading to backend
// and this requests represents second half of uploadable effort. Else this represents all (no previous heavy requests)
const progressRepresents = isPasswordEncryptedMessage ? 'SECOND-HALF' : 'EVERYTHING';`
msgSentRes = await this.view.emailProvider.msgSend(msg, (p) => this.renderUploadProgress(p, progressRepresents));You can start implementation with approximately this method:
private getPwdMsgSendableBodyWithOnlineReplyMsgToken = async (
authInfo: FcUuidAuth, newMsgData: NewMsgData
): Promise<{ bodyWithReplyToken: SendableMsgBody, replyToken: string }> => {
const recipients = Array.prototype.concat.apply([], Object.values(newMsgData.recipients));
try {
const response = await this.view.acctServer.messageToken(authInfo);
const infoDiv = Ui.e('div', {
'style': 'display: none;',
'class': 'cryptup_reply',
'cryptup-data': Str.htmlAttrEncode({
sender: newMsgData.from,
recipient: Value.arr.withoutVal(Value.arr.withoutVal(recipients, newMsgData.from), this.acctEmail),
subject: newMsgData.subject,
token: response.replyToken,
})
});
return {
bodyWithReplyToken: { 'text/plain': newMsgData.plaintext + '\n\n' + infoDiv, 'text/html': newMsgData.plainhtml + '<br /><br />' + infoDiv },
replyToken: response.replyToken
};
} catch (msgTokenErr) {
if (ApiErr.isAuthErr(msgTokenErr)) {
Settings.offerToLoginWithPopupShowModalOnErr(this.acctEmail);
throw new ComposerResetBtnTrigger();
} else if (ApiErr.isNetErr(msgTokenErr)) {
throw msgTokenErr;
}
// note - you don't need to re-implement this exactly
throw Catch.rewrapErr(msgTokenErr, 'There was a token error sending this message. Please try again. Let us know at human@flowcrypt.com if this happens repeatedly.');
}
};Rendering upload progress:
public renderUploadProgress = (progress: number | undefined, progressRepresents: 'FIRST-HALF' | 'SECOND-HALF' | 'EVERYTHING') => {
if (progress && this.view.attachmentsModule.attachment.hasAttachment()) {
if (progressRepresents === 'FIRST-HALF') {
progress = Math.floor(progress / 2); // show 0-50% instead of 0-100%
} else if (progressRepresents === 'SECOND-HALF') {
progress = Math.floor(50 + progress / 2); // show 50-100% instead of 0-100%
} else {
progress = Math.floor(progress); // show 0-100%
}
this.view.S.now('send_btn_text').text(`${SendBtnTexts.BTN_SENDING} ${progress < 100 ? `${progress}%` : ''}`);
}
};Formatting plain part of Gmail message
private formatPwdEncryptedMsgBodyLink = async (msgUrl: string): Promise<SendableMsgBody> => {
const storage = await AcctStore.get(this.acctEmail, ['outgoing_language']);
const lang = storage.outgoing_language || 'EN';
const aStyle = `padding: 2px 6px; background: #2199e8; color: #fff; display: inline-block; text-decoration: none;`;
const a = `<a href="${Xss.escape(msgUrl)}" style="${aStyle}">${Lang.compose.openMsg[lang]}</a>`;
const intro = this.view.S.cached('input_intro').length ? this.view.inputModule.extract('text', 'input_intro') : undefined;
const text = [];
const html = [];
if (intro) {
text.push(intro + '\n');
html.push(Xss.escape(intro).replace(/\n/g, '<br>') + '<br><br>');
}
const senderEmail = Xss.escape(this.view.senderModule.getSender());
text.push(Lang.compose.msgEncryptedText(lang, senderEmail) + msgUrl + '\n\n');
html.push(`${Lang.compose.msgEncryptedHtml(lang, senderEmail) + a}<br/><br/>${Lang.compose.alternativelyCopyPaste[lang] + Xss.escape(msgUrl)}<br/><br/>`);
return { 'text/plain': text.join('\n'), 'text/html': html.join('\n') };
};Formatting final Gmail message:
private sendablePwdMsg = async (newMsg: NewMsgData, pubs: PubkeyResult[], msgUrl: string, signingPrv?: Key) => {
// encoded as: PGP/MIME-like structure but with attachments as external files due to email size limit (encrypted for pubkeys only)
const msgBody = this.richtext ? { 'text/plain': newMsg.plaintext, 'text/html': newMsg.plainhtml } : { 'text/plain': newMsg.plaintext };
const pgpMimeNoAttachments = await Mime.encode(msgBody, { Subject: newMsg.subject }, []); // no attachments, attached to email separately
const { data: pubEncryptedNoAttachments } = await this.encryptDataArmor(Buf.fromUtfStr(pgpMimeNoAttachments), undefined, pubs, signingPrv); // encrypted only for pubs
const attachments = this.createPgpMimeAttachments(pubEncryptedNoAttachments).
concat(await this.view.attachmentsModule.attachment.collectEncryptAttachments(pubs)); // encrypted only for pubs
const emailIntroAndLinkBody = await this.formatPwdEncryptedMsgBodyLink(msgUrl);
return await SendableMsg.createPwdMsg(this.acctEmail, this.headers(newMsg), emailIntroAndLinkBody, attachments, { isDraft: this.isDraft });
};Reactions are currently unavailable