diff --git a/extension/css/cryptup.css b/extension/css/cryptup.css index f15e96959a7..3ccd03484df 100644 --- a/extension/css/cryptup.css +++ b/extension/css/cryptup.css @@ -3550,6 +3550,64 @@ body#settings div.webmail_notifications div.webmail_notification a:hover { text-decoration: none; } +/* thunderbird ui css */ + +.thunderbird_attachment_root { + margin: 0 7px 7px 0; + padding: 6px 4px; + background: #fff; + border: 1px solid #d8d8d8; + color: #444; + text-overflow: ellipsis; + align-items: center; + align-content: center; + width: 240px; + max-width: 270px; + display: inline-flex; + cursor: pointer; + position: relative; + border-radius: 2px; + height: 40px; + font-size: 16px; +} + +.thunderbird_attachment_root div { + display: inline-block; +} + +.thunderbird_attachment_root .thunderbird_attachment_name { + margin: 0 2px; +} + +.thunderbird_attachment_root .thunderbird_attachment_icon { + height: 40px; + width: 40px; + background: #fff; + overflow: hidden; + opacity: 0.4; +} + +.thunderbird_attachment_root .thunderbird_attachment_download { + display: none; + padding: 6px; + height: 30px; + width: 30px; + right: 0; + position: absolute; + background: #333; + opacity: 0.88; + border-radius: 2px; + margin: 3px 6px; +} + +.thunderbird_attachment_root:hover .thunderbird_attachment_download { + display: inline-block; +} + +.thunderbird_attachment_download img { + width: 100%; +} + /* print class */ @media screen { .printable { diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 5a58badb9a8..90ade45c02d 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -20,6 +20,7 @@ import { Ui } from './ui.js'; import { AuthRes } from '../api/authentication/generic/oauth.js'; import { GlobalStore } from '../platform/store/global-store.js'; import { BgUtils } from '../../service_worker/bgutils.js'; +import { ThunderbirdAttachment } from '../core/attachment.js'; export type GoogleAuthWindowResult$result = 'Success' | 'Denied' | 'Error' | 'Closed'; export type ScreenDimensions = { width: number; height: number; availLeft: number; availTop: number }; @@ -98,6 +99,7 @@ export namespace Bm { export type PgpBlockRetry = { frameId: string; messageSender: Dest }; export type PgpBlockReady = { frameId: string; messageSender: Dest }; export type ThunderbirdOpenPassphraseDialog = { acctEmail: string; longids: string }; + export type ThunderbirdInitiateAttachmentDownload = { decryptedFileName: string; decryptedContent: Buf }; export namespace Res { export type GetActiveTabInfo = { @@ -114,9 +116,10 @@ export namespace Bm { export type ExpirationCacheGet = Promise; export type ExpirationCacheSet = Promise; export type ExpirationCacheDeleteExpired = Promise; + export type ThunderbirdGetDownloadableAttachment = { from: string; processableAttachments: ThunderbirdAttachment[] }; export type ThunderbirdGetCurrentUser = string | undefined; - export type ThunderbirdMsgGet = messenger.messages.MessagePart | undefined; export type ThunderbirdOpenPassphraseDialog = Promise; + export type ThunderbirdInitiateAttachmentDownload = Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Db = any; // not included in Any below // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -134,7 +137,7 @@ export namespace Bm { | ExpirationCacheDeleteExpired | AjaxGmailAttachmentGetChunk | ConfirmationResult - | ThunderbirdMsgGet; + | ThunderbirdGetDownloadableAttachment; } export type AnyRequest = @@ -176,6 +179,7 @@ export namespace Bm { | PgpBlockRetry | ConfirmationResult | ThunderbirdOpenPassphraseDialog + | ThunderbirdInitiateAttachmentDownload | Ajax; export type AsyncRespondingHandler = (req: AnyRequest) => Promise; @@ -239,9 +243,12 @@ export class BrowserMsg { BrowserMsg.sendAwait(undefined, 'expirationCacheSet', bm, true) as Promise, expirationCacheDeleteExpired: (bm: Bm.ExpirationCacheDeleteExpired) => BrowserMsg.sendAwait(undefined, 'expirationCacheDeleteExpired', bm, true) as Promise, + thunderbirdGetDownloadableAttachment: () => + BrowserMsg.sendAwait(undefined, 'thunderbirdGetDownloadableAttachment', undefined, true) as Promise, + thunderbirdInitiateAttachmentDownload: (bm: Bm.ThunderbirdInitiateAttachmentDownload) => + BrowserMsg.sendAwait(undefined, 'thunderbirdInitiateAttachmentDownload', bm, true) as Promise, thunderbirdGetCurrentUser: () => BrowserMsg.sendAwait(undefined, 'thunderbirdGetCurrentUser', undefined, true) as Promise, - thunderbirdMsgGet: () => BrowserMsg.sendAwait(undefined, 'thunderbirdMsgGet', undefined, true) as Promise, thunderbirdOpenPassphraseDiaglog: (bm: Bm.ThunderbirdOpenPassphraseDialog) => BrowserMsg.sendAwait(undefined, 'thunderbirdOpenPassphraseDialog', bm, true) as Promise, }, diff --git a/extension/js/common/core/attachment.ts b/extension/js/common/core/attachment.ts index 80fbe132116..fffd67f84fa 100644 --- a/extension/js/common/core/attachment.ts +++ b/extension/js/common/core/attachment.ts @@ -29,6 +29,12 @@ export type AttachmentProperties = { contentDescription?: string; contentTransferEncoding?: ContentTransferEncoding; }; +export type ThunderbirdAttachment = { + name: string; + contentType: string; + data: Buf; + treatAs: Attachment$treatAs; +}; export type AttachmentMeta = (AttachmentId | { data: Uint8Array }) & AttachmentProperties; export type FcAttachmentLinkData = { name: string; type: string; size: number }; diff --git a/extension/js/content_scripts/webmail/thunderbird/thunderbird-element-replacer.ts b/extension/js/content_scripts/webmail/thunderbird/thunderbird-element-replacer.ts index ca24d40322d..840c7fde306 100644 --- a/extension/js/content_scripts/webmail/thunderbird/thunderbird-element-replacer.ts +++ b/extension/js/content_scripts/webmail/thunderbird/thunderbird-element-replacer.ts @@ -3,11 +3,12 @@ 'use strict'; import { BrowserMsg } from '../../../common/browser/browser-msg.js'; +import { Attachment, ThunderbirdAttachment } from '../../../common/core/attachment.js'; +import { Buf } from '../../../common/core/buf.js'; import { KeyUtil } from '../../../common/core/crypto/key.js'; -import { DecryptError, DecryptErrTypes, MsgUtil } from '../../../common/core/crypto/pgp/msg-util.js'; +import { DecryptError, DecryptErrTypes, MsgUtil, VerifyRes } from '../../../common/core/crypto/pgp/msg-util.js'; import { OpenPGPKey } from '../../../common/core/crypto/pgp/openpgp-key.js'; import { PgpArmor } from '../../../common/core/crypto/pgp/pgp-armor'; -import { Catch } from '../../../common/platform/catch'; import { ContactStore } from '../../../common/platform/store/contact-store.js'; import { KeyStore } from '../../../common/platform/store/key-store.js'; import { Xss } from '../../../common/platform/xss.js'; @@ -20,71 +21,110 @@ export class ThunderbirdElementReplacer extends WebmailElementReplacer { public scrollToReplyBox: (replyMsgId: string) => void; public scrollToCursorInReplyBox: (replyMsgId: string, cursorOffsetTop: number) => void; public addSecureActionsToMessageMenu: () => void; + private acctEmail: string; private emailBodyFromThunderbirdMail: string; public getIntervalFunctions = (): IntervalFunction[] => { - return [{ interval: 2000, handler: () => this.replaceThunderbirdMsgPane() }]; + return [{ interval: 2000, handler: () => this.handleThunderbirdMessageParsing() }]; }; - public replaceThunderbirdMsgPane = async () => { - if (Catch.isThunderbirdMail()) { - const fullMsg = await BrowserMsg.send.bg.await.thunderbirdMsgGet(); - if (!fullMsg) { - return; - } else { - const acctEmail = await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser(); - const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, String(acctEmail)))?.sortedPubkeys ?? []; - const signerKeys = parsedPubs.map(key => KeyUtil.armor(key.pubkey)); - if (this.isPublicKeyEncryptedMsg(fullMsg)) { - const result = await MsgUtil.decryptMessage({ - kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(String(acctEmail)), - encryptedData: this.emailBodyFromThunderbirdMail, - verificationPubs: signerKeys, - }); - if (result.success && result.content) { - const decryptedMsg = result.content.toUtfStr(); - const encryptionStatus = result.isEncrypted ? 'encrypted' : 'not encrypted'; - let verificationStatus = ''; - if (result?.signature) { - if (result.signature.match) { - verificationStatus = 'signed'; - } else if (result.signature.error) { - verificationStatus = `could not verify signature: ${result.signature.error}`; - } else { - verificationStatus = 'not signed'; - } - } - const pgpBlock = this.generatePgpBlockTemplate(encryptionStatus, verificationStatus, decryptedMsg); - $('body').html(pgpBlock); // xss-sanitized - } else { - const decryptErr = result as DecryptError; - let decryptionErrorMsg = ''; - if (decryptErr.error && decryptErr.error.type === DecryptErrTypes.needPassphrase) { - const acctEmail = String(await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser()); - const longids = decryptErr.longids.needPassphrase.join(','); - decryptionErrorMsg = `decrypt error: private key needs to be unlocked by your passphrase.`; - await BrowserMsg.send.bg.await.thunderbirdOpenPassphraseDiaglog({ acctEmail, longids }); - } else { - decryptionErrorMsg = `decrypt error: ${(result as DecryptError).error.message}`; - } - const pgpBlock = this.generatePgpBlockTemplate(decryptionErrorMsg, 'not signed', this.emailBodyFromThunderbirdMail); - $('body').html(pgpBlock); // xss-sanitized - } - } else if (this.isCleartextMsg(fullMsg)) { - const message = await openpgp.readCleartextMessage({ cleartextMessage: this.emailBodyFromThunderbirdMail }); - const result = await OpenPGPKey.verify(message, await ContactStore.getPubkeyInfos(undefined, signerKeys)); - let verificationStatus = ''; - let signedMessage = ''; - if (result.match && result.content) { - verificationStatus = 'signed'; - signedMessage = result.content.toUtfStr(); - } else if (result.error) { - verificationStatus = `could not verify signature: ${result.error}`; - } - const pgpBlock = this.generatePgpBlockTemplate('not encrypted', verificationStatus, signedMessage); - $('body').html(pgpBlock); // xss-sanitized + public handleThunderbirdMessageParsing = async () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.acctEmail = (await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser())!; + const emailBodyToParse = $('div.moz-text-plain').text().trim() || $('div.moz-text-html').text().trim() || $('div.moz-text-flowed').text().trim(); + const { processableAttachments: fcAttachments, from: signerEmail } = await BrowserMsg.send.bg.await.thunderbirdGetDownloadableAttachment(); + const parsedPubs = (await ContactStore.getOneWithAllPubkeys(undefined, signerEmail))?.sortedPubkeys ?? []; + const verificationPubs = parsedPubs.map(key => KeyUtil.armor(key.pubkey)); + if (this.resemblesAsciiArmoredMsg(emailBodyToParse)) { + await this.messageDecrypt(verificationPubs, this.emailBodyFromThunderbirdMail); + } else if (this.resemblesSignedMsg(emailBodyToParse)) { + await this.messageVerify(verificationPubs); + } + if (fcAttachments.length) { + for (const fcAttachment of fcAttachments) { + await this.attachmentUiRenderer(fcAttachment, verificationPubs, emailBodyToParse); + } + } + $('body').show(); + }; + + private messageDecrypt = async (verificationPubs: string[], encryptedData: string | Buf) => { + $('body').hide(); + const result = await MsgUtil.decryptMessage({ + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), + encryptedData, + verificationPubs, + }); + if (result.success && result.content) { + const pgpBlockContent = result.content.toUtfStr(); + const encryptionStatus = result.isEncrypted ? 'encrypted' : 'not encrypted'; + let verificationStatus = ''; + if (result?.signature) { + if (result.signature.match) { + verificationStatus = 'signed'; + } else if (result.signature.error) { + verificationStatus = `could not verify signature: ${result.signature.error}`; + } else { + verificationStatus = 'not signed'; } - // todo: detached signed message via https://github.com/FlowCrypt/flowcrypt-browser/issues/5668 + } + const pgpBlock = this.generatePgpBlockTemplate(encryptionStatus, verificationStatus, pgpBlockContent); + $('body').html(pgpBlock); // xss-sanitized + } else { + const decryptErr = result as DecryptError; + let decryptionErrorMsg = ''; + if (decryptErr.error && decryptErr.error.type === DecryptErrTypes.needPassphrase) { + const acctEmail = String(await BrowserMsg.send.bg.await.thunderbirdGetCurrentUser()); + const longids = decryptErr.longids.needPassphrase.join(','); + decryptionErrorMsg = `decrypt error: private key needs to be unlocked by your passphrase.`; + await BrowserMsg.send.bg.await.thunderbirdOpenPassphraseDiaglog({ acctEmail, longids }); + } else { + decryptionErrorMsg = `decrypt error: ${(result as DecryptError).error.message}`; + } + const pgpBlock = this.generatePgpBlockTemplate(decryptionErrorMsg, 'not signed', this.emailBodyFromThunderbirdMail); + $('body').html(pgpBlock); // xss-sanitized + } + }; + + private messageVerify = async (verificationPubs: string[], detachedSignatureParams?: { plaintext: string; sigText: string }) => { + $('body').hide(); + let result: VerifyRes; + if (!detachedSignatureParams) { + const message = await openpgp.readCleartextMessage({ cleartextMessage: this.emailBodyFromThunderbirdMail }); + result = await OpenPGPKey.verify(message, await ContactStore.getPubkeyInfos(undefined, verificationPubs)); + } else { + result = await MsgUtil.verifyDetached({ plaintext: detachedSignatureParams.plaintext, sigText: detachedSignatureParams.sigText, verificationPubs }); + } + let verificationStatus = ''; + let pgpBlockContent = ''; + if (result.content) { + verificationStatus = result.match ? 'signed' : 'not signed'; + if (!result.signerLongids.length) { + verificationStatus = `could not verify signature: missing pubkey ${result.signerLongids}`; + } + pgpBlockContent = result.content.toUtfStr(); + } else if (result.error) { + verificationStatus = `could not verify signature: ${result.error}`; + pgpBlockContent = detachedSignatureParams?.plaintext || this.emailBodyFromThunderbirdMail; + } + const pgpBlock = this.generatePgpBlockTemplate('not encrypted', verificationStatus, pgpBlockContent); + $('body').html(pgpBlock); // xss-sanitized + }; + + private attachmentUiRenderer = async (fcAttachment: ThunderbirdAttachment, verificationPubs: string[], emailBodyToParse: string) => { + if (fcAttachment.treatAs === 'encryptedFile' && emailBodyToParse) { + const generatedPgpTemplate = this.generatePgpAttachmentTemplate(fcAttachment.name, fcAttachment.data); + $('.pgp_attachments_block').append(generatedPgpTemplate); // xss-sanitized + } else if ( + (fcAttachment.treatAs === 'encryptedMsg' || Attachment.encryptedMsgNames.some(a => fcAttachment.name.includes(a))) && + !this.emailBodyFromThunderbirdMail + ) { + await this.messageDecrypt(verificationPubs, fcAttachment.data); + // detached signature verification + } else if (fcAttachment.treatAs === 'signature') { + const sigText = new TextDecoder('utf-8').decode(fcAttachment.data).trim(); + if (this.resemblesSignedMsg(sigText)) { + await this.messageVerify(verificationPubs, { plaintext: emailBodyToParse, sigText: sigText.replace('\n=3D', '\n=') }); } } }; @@ -99,48 +139,60 @@ export class ThunderbirdElementReplacer extends WebmailElementReplacer {
${Xss.escape(messageToRender)}
+
+
`; }; - private isCleartextMsg = (fullMsg: messenger.messages.MessagePart): boolean => { - return ( - (fullMsg.headers && - 'openpgp' in fullMsg.headers && - fullMsg.parts && - fullMsg.parts[0]?.parts?.length === 1 && - fullMsg.parts[0].parts[0].contentType === 'text/plain' && - this.resemblesCleartextMsg(fullMsg.parts[0].parts[0].body?.trim() || '')) || - false - ); + private generatePgpAttachmentTemplate = (originalFilename: string, attachmentData: Buf) => { + const attachmentHtmlRoot = $('
').addClass('thunderbird_attachment_root'); + const attachmentFileTypeIcon = $('').addClass('thunderbird_attachment_icon'); + const decryptedFileName = originalFilename.replace(/\.(pgp|gpg|asc)$/i, ''); + const uiFileExtensions = ['excel', 'word', 'png', 'jpg']; + const matchedExtension = uiFileExtensions.find(fileExtension => decryptedFileName.endsWith(fileExtension)); + attachmentFileTypeIcon.attr('src', messenger.runtime.getURL(`/img/fileformat/${matchedExtension || 'generic'}.png`)); + const attachmentFilename = $('
').addClass('thunderbird_attachment_name').text(originalFilename); + const attachmentDownloadBtn = $('
') + .addClass('thunderbird_attachment_download') + .on('click', async () => { + await this.downloadThunderbirdAttachmentHandler(decryptedFileName, attachmentData); + }) + .append($('').attr('src', messenger.runtime.getURL('/img/svgs/download-link.svg'))); // xss-safe-value + attachmentHtmlRoot.append(attachmentFileTypeIcon); // xss-escaped + attachmentHtmlRoot.append(attachmentFilename); // xss-safe-value + attachmentHtmlRoot.append(attachmentDownloadBtn); // xss-safe-value + return attachmentHtmlRoot; + }; + + private downloadThunderbirdAttachmentHandler = async (decryptedFileName: string, encryptedData: Buf) => { + const result = await MsgUtil.decryptMessage({ + kisWithPp: await KeyStore.getAllWithOptionalPassPhrase(this.acctEmail), + encryptedData, + verificationPubs: [], // todo: #4158 signature verification of attachments + }); + if (result.success && result.content) { + await BrowserMsg.send.bg.await.thunderbirdInitiateAttachmentDownload({ decryptedFileName, decryptedContent: result.content }); + } + // no need to handle DecryptErrTypes.needPassphrase it was already handled by this.messageDecrypt() }; - private resemblesCleartextMsg = (body: string) => { + private resemblesSignedMsg = (body: string) => { this.emailBodyFromThunderbirdMail = body; return ( - body.startsWith(PgpArmor.ARMOR_HEADER_DICT.signedMsg.begin) && - body.includes(String(PgpArmor.ARMOR_HEADER_DICT.signedMsg.middle)) && - body.endsWith(String(PgpArmor.ARMOR_HEADER_DICT.signedMsg.end)) + (body.startsWith(PgpArmor.ARMOR_HEADER_DICT.signedMsg.begin) && + body.includes(String(PgpArmor.ARMOR_HEADER_DICT.signedMsg.middle)) && + body.endsWith(String(PgpArmor.ARMOR_HEADER_DICT.signedMsg.end))) || + (body.startsWith(PgpArmor.ARMOR_HEADER_DICT.signature.begin) && body.endsWith(String(PgpArmor.ARMOR_HEADER_DICT.signature.end))) ); }; - private isPublicKeyEncryptedMsg = (fullMsg: messenger.messages.MessagePart): boolean => { - if (fullMsg.headers && 'openpgp' in fullMsg.headers && fullMsg.parts) { - return ( - (fullMsg.parts[0]?.parts?.length === 2 && - fullMsg.parts[0]?.parts[1].contentType === 'application/pgp-encrypted' && - this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.parts[0].body?.trim() || '')) || - (fullMsg.parts[0]?.parts?.length === 1 && - fullMsg.parts[0]?.contentType === 'multipart/mixed' && - this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.parts[0].body?.trim() || '')) || - (fullMsg.parts.length === 1 && fullMsg.parts[0]?.contentType === 'text/plain' && this.resemblesAsciiArmoredMsg(fullMsg.parts[0]?.body?.trim() || '')) || - false - ); + private resemblesAsciiArmoredMsg = (body: string): boolean => { + const pgpRegex = /-----BEGIN PGP MESSAGE-----(.*?)-----END PGP MESSAGE-----/s; + const pgpRegexMatch = new RegExp(pgpRegex).exec(body); + if (pgpRegexMatch?.[0]) { + this.emailBodyFromThunderbirdMail = pgpRegexMatch[0]; + return true; } return false; }; - - private resemblesAsciiArmoredMsg = (body: string): boolean => { - this.emailBodyFromThunderbirdMail = body; - return body.startsWith(PgpArmor.ARMOR_HEADER_DICT.encryptedMsg.begin) && body.endsWith(PgpArmor.ARMOR_HEADER_DICT.encryptedMsg.end as string); - }; } diff --git a/extension/js/content_scripts/webmail/thunderbird/thunderbird-webmail-startup.ts b/extension/js/content_scripts/webmail/thunderbird/thunderbird-webmail-startup.ts index ae9f803a6db..34b68f7c2dd 100644 --- a/extension/js/content_scripts/webmail/thunderbird/thunderbird-webmail-startup.ts +++ b/extension/js/content_scripts/webmail/thunderbird/thunderbird-webmail-startup.ts @@ -19,7 +19,8 @@ export class ThunderbirdWebmailStartup { private start = async () => { this.replacer = new ThunderbirdElementReplacer(); - this.replacer.runIntervalFunctionsPeriodically(); + // doesn't need hearbeat-like content replacer as the extension noticeably slows the Thunderbird client. + await this.replacer.handleThunderbirdMessageParsing(); // todo: show notification using Thunderbird Notification as contentscript notification or such does not work. // await notifications.showInitial(acctEmail); // notifications.show( diff --git a/extension/js/service_worker/background.ts b/extension/js/service_worker/background.ts index 0ecf489c014..0a356c2586e 100644 --- a/extension/js/service_worker/background.ts +++ b/extension/js/service_worker/background.ts @@ -74,7 +74,8 @@ console.info('background.js service worker starting'); BgHandlers.thunderbirdSecureComposeHandler(); await BgHandlers.thunderbirdContentScriptRegistration(); BrowserMsg.bgAddListener('thunderbirdGetCurrentUser', BgHandlers.thunderbirdGetCurrentUserHandler); - BrowserMsg.bgAddListener('thunderbirdMsgGet', BgHandlers.thunderbirdMsgGetHandler); + BrowserMsg.bgAddListener('thunderbirdGetDownloadableAttachment', BgHandlers.thunderbirdGetDownloadableAttachment); + BrowserMsg.bgAddListener('thunderbirdInitiateAttachmentDownload', BgHandlers.thunderbirdInitiateAttachmentDownload); BrowserMsg.bgAddListener('thunderbirdOpenPassphraseDialog', BgHandlers.thunderbirdOpenPassphraseDialog); } })().catch(Catch.reportErr); diff --git a/extension/js/service_worker/bg-handlers.ts b/extension/js/service_worker/bg-handlers.ts index ce2b57c3bfc..fa8bb743b68 100644 --- a/extension/js/service_worker/bg-handlers.ts +++ b/extension/js/service_worker/bg-handlers.ts @@ -13,6 +13,7 @@ import { GoogleOAuth } from '../common/api/authentication/google/google-oauth.js import { AcctStore } from '../common/platform/store/acct-store.js'; import { ConfiguredIdpOAuth } from '../common/api/authentication/configured-idp-oauth.js'; import { Url, Str } from '../common/core/common.js'; +import { Attachment, ThunderbirdAttachment } from '../common/core/attachment.js'; export class BgHandlers { public static openSettingsPageHandler: Bm.AsyncResponselessHandler = async ({ page, path, pageUrlParams, addNewAcct, acctEmail }: Bm.Settings) => { @@ -160,6 +161,57 @@ export class BgHandlers { }); }; + public static thunderbirdGetDownloadableAttachment = async (): Promise => { + const processableAttachments: ThunderbirdAttachment[] = []; + const [tab] = await messenger.mailTabs.query({ active: true, currentWindow: true }); + const message = await messenger.messageDisplay.getDisplayedMessage(tab.id); + let from = ''; + if (tab.id && message?.id) { + from = Str.parseEmail(message.author).email || ''; + const mimeMsg = await messenger.messages.getFull(message.id); + let attachments = await messenger.messages.listAttachments(message.id); + const fcAttachments: Attachment[] = []; + if (mimeMsg.parts?.[0].contentType === 'multipart/signed' && mimeMsg.parts?.[0].parts?.length === 2) { + attachments = attachments.filter(file => file.contentType === 'application/pgp-signature'); + } + // convert Thunderbird Attachments to FlowCrypt recognizable Attachments + for (const attachment of attachments) { + const file = await messenger.messages.getAttachmentFile(message.id, attachment.partName); + fcAttachments.push( + new Attachment({ + data: new Uint8Array(await file.arrayBuffer()), + type: attachment.contentType, + name: attachment.name, + length: attachment.size, + }) + ); + } + for (const fcAttachment of fcAttachments) { + processableAttachments.push({ + name: fcAttachment.name, + contentType: fcAttachment.type, + data: fcAttachment.getData(), + treatAs: fcAttachment.treatAs(fcAttachments), + }); + } + } + return { from, processableAttachments }; + }; + + public static thunderbirdInitiateAttachmentDownload = async ( + r: Bm.ThunderbirdInitiateAttachmentDownload + ): Promise => { + // todo - add prompt using messenger.notifications.create. requires `notifications` permission; + const blob = new Blob([r.decryptedContent]); + const fileUrl = URL.createObjectURL(blob); + await browser.downloads.download({ + url: fileUrl, + filename: r.decryptedFileName, + saveAs: true, + }); + URL.revokeObjectURL(fileUrl); + }; + public static thunderbirdGetCurrentUserHandler = async (): Promise => { const [tab] = await messenger.tabs.query({ active: true, currentWindow: true }); if (tab.id) { @@ -170,17 +222,6 @@ export class BgHandlers { return; }; - public static thunderbirdMsgGetHandler = async (): Promise => { - const [tab] = await messenger.tabs.query({ active: true, currentWindow: true }); - if (tab.id) { - const message = await messenger.messageDisplay.getDisplayedMessage(tab.id); - if (message?.id) { - return await messenger.messages.getFull(Number(message.id)); - } - } - return; - }; - public static thunderbirdOpenPassphraseDialog = async (r: Bm.ThunderbirdOpenPassphraseDialog): Promise => { await BgUtils.openExtensionTab(`chrome/elements/passphrase.htm?type=message&parentTabId=0&acctEmail=${r.acctEmail}&longids=${r.longids}`, true); }; diff --git a/test/source/patterns.ts b/test/source/patterns.ts index 1bee94d8132..d5905005445 100644 --- a/test/source/patterns.ts +++ b/test/source/patterns.ts @@ -113,7 +113,7 @@ for (const buildType of ['chrome-consumer', 'chrome-enterprise', 'thunderbird-co console.error(`${buildType} - The content_security_policy should be a string`); errsFound++; } - const thunderbirdExpectedPermissions = ['compose', 'messagesRead', 'messagesUpdate', 'messagesModify', 'accountsRead']; + const thunderbirdExpectedPermissions = ['compose', 'downloads', 'downloads.open', 'messagesRead', 'messagesUpdate', 'messagesModify', 'accountsRead']; const buildHostPermissions = isManifestV3Build ? manifest.host_permissions : manifest.permissions; for (const expectedHostPermission of thunderbirdExpectedPermissions) { if (!buildHostPermissions?.includes(expectedHostPermission)) { diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index 51ad9d64217..d8730fdba04 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -52,8 +52,16 @@ addManifest( manifest.manifest_version = 2; manifest.name = 'FlowCrypt Encryption for Thunderbird'; manifest.description = 'Simple end-to-end encryption to secure email and attachments on Thunderbird'; - manifest.permissions = [...(manifest.permissions ?? []), 'compose', 'messagesRead', 'messagesUpdate', 'messagesModify', 'accountsRead']; - + manifest.permissions = [ + ...(manifest.permissions ?? []), + 'compose', + 'downloads', + 'downloads.open', + 'messagesRead', + 'messagesUpdate', + 'messagesModify', + 'accountsRead', + ]; const manifestV3 = manifest as chrome.runtime.ManifestV3; manifest.web_accessible_resources = manifestV3.web_accessible_resources?.[0].resources; manifest.content_security_policy = manifestV3.content_security_policy?.extension_pages;