From c47d82d0b5c3feee42c50d06cc9abca003eb8bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20D=C3=B6rfelt?= Date: Wed, 29 May 2024 17:10:05 +0200 Subject: [PATCH 1/3] add simple debug messages --- __tests__/test_background.ts | 17 +++++++++++++ src/background.ts | 49 ++++++++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/__tests__/test_background.ts b/__tests__/test_background.ts index 7470376..5e52399 100644 --- a/__tests__/test_background.ts +++ b/__tests__/test_background.ts @@ -12,6 +12,7 @@ import { getAndProcessMessages, handleContextMenu, handleHotkey, + handleMenuButton, onlyWhitespace, processMail, renderString, @@ -274,6 +275,22 @@ describe("handle button / hotkey / context menu", () => { } ); + test("export by menu button", async () => { + messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); + browser.messageDisplay.getDisplayedMessages.mockReturnValueOnce([ + { id: 1 }, + ]); + + await handleMenuButton({ id: 1 }, { menuItemId: "export_to_joplin" }); + + expect(requests.length).toBe(1); + expectConsole({ + log: 1, + warn: 0, + error: 0, + }); + }); + test("export by hotkey", async () => { messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); browser.messageDisplay.getDisplayedMessages.mockReturnValueOnce([ diff --git a/src/background.ts b/src/background.ts index 57f1c59..dd483ed 100644 --- a/src/background.ts +++ b/src/background.ts @@ -4,6 +4,15 @@ import { generateUrl, getSetting } from "./common"; declare const browser: any; declare const messenger: any; +////////////////////////////////////////////////// +// Export by menu button +////////////////////////////////////////////////// + +async function handleMenuButton(tab: { id: number }, info: any) { + console.debug("[Joplin Export] Export via menu button."); + await getAndProcessMessages(tab, {}); +} + ////////////////////////////////////////////////// // Export by context menu ////////////////////////////////////////////////// @@ -20,6 +29,7 @@ async function handleContextMenu( tab: { id: number } ) { if (info.menuItemId === "export_to_joplin") { + console.debug("[Joplin Export] Export via context menu."); await getAndProcessMessages(tab, {}); } } @@ -32,6 +42,7 @@ async function handleHotkey(command: string) { // Called if hotkey is pressed. if (command === "export_to_joplin") { + console.debug("[Joplin Export] Export via hotkey."); // Only the active tab is queried. So the array contains always exactly one element. const [activeTab] = await messenger.tabs.query({ active: true, @@ -73,7 +84,7 @@ function renderString(inputString: string, context: { [key: string]: any }) { } ////////////////////////////////////////////////// -// Export by menu button +// Main export function ////////////////////////////////////////////////// async function getAndProcessMessages(tab: { id: number }, info: any) { @@ -93,12 +104,15 @@ async function getAndProcessMessages(tab: { id: number }, info: any) { const mailHeaders = await browser.messageDisplay.getDisplayedMessages( tab.id ); + console.debug( + `[Joplin Export] Got ${mailHeaders.length} emails at tab ${tab.id}.` + ); // Process the mails and check for success. const results = await Promise.all(mailHeaders.map(processMail)); for (const error of results) { if (error) { - console.error(error); + console.error(`[Joplin Export] ${error}`); success = false; } } @@ -136,7 +150,7 @@ async function processMail(mailHeader: any) { // Mail content ////////////////////////////////////////////////// - // https://webextension-api.thunderbird.net/en/91/messages.html#messages-messageheader + // https://webextension-api.thunderbird.net/en/latest/messages.html#messages-messageheader if (!mailHeader) { return "Mail header is empty"; } @@ -228,15 +242,15 @@ async function processMail(mailHeader: any) { // If the preferred content type doesn't contain data, fall back to the other content type. const contentType = await getSetting("joplinNoteFormat"); if ((contentType === "text/html" && mailBodyHtml) || !mailBodyPlain) { - console.log("Sending complete email in HTML format."); + console.log("[Joplin Export] Sending complete email in HTML format."); data["body_html"] = mailBodyHtml; } if ((contentType === "text/plain" && mailBodyPlain) || !mailBodyHtml) { - console.log("Sending complete email in plain format."); + console.log("[Joplin Export] Sending complete email in plain format."); data["body"] = mailBodyPlain; } } else { - console.log("Sending selection in plain format."); + console.log("[Joplin Export] Sending selection in plain format."); data["body"] = selectedText; } @@ -298,7 +312,9 @@ async function processMail(mailHeader: any) { url = await generateUrl("search", [`query=${strippedTag}`, "type=tag"]); response = await fetch(url); if (!response.ok) { - console.warn(`Search for tag failed: ${await response.text()}`); + console.warn( + `[Joplin Export] Search for tag failed: ${await response.text()}` + ); continue; } const searchResult = await response.json(); @@ -314,7 +330,9 @@ async function processMail(mailHeader: any) { body: JSON.stringify({ title: strippedTag }), }); if (!response.ok) { - console.warn(`Failed to create tag: ${await response.text()}`); + console.warn( + `[Joplin Export] Failed to create tag: ${await response.text()}` + ); continue; } const tagInfo = await response.json(); @@ -327,7 +345,7 @@ async function processMail(mailHeader: any) { .map((e: { id: string; title: string }) => e.title) .join(", "); console.warn( - `Too many matching tags for "${strippedTag}": ${matchingTagsString}` + `[Joplin Export] Too many matching tags for "${strippedTag}": ${matchingTagsString}` ); continue; } @@ -340,7 +358,9 @@ async function processMail(mailHeader: any) { body: JSON.stringify({ id: noteInfo["id"] }), }); if (!response.ok) { - console.warn(`Failed to attach tag to note: ${await response.text()}`); + console.warn( + `[Joplin Export] Failed to attach tag to note: ${await response.text()}` + ); continue; // not necessary, but added in case of a future refactoring } } @@ -374,7 +394,9 @@ async function processMail(mailHeader: any) { body: formData, }); if (!response.ok) { - console.warn(`Failed to create resource: ${await response.text()}`); + console.warn( + `[Joplin Export] Failed to create resource: ${await response.text()}` + ); continue; } const resourceInfo = await response.json(); @@ -390,7 +412,7 @@ async function processMail(mailHeader: any) { }); if (!response.ok) { console.warn( - `Failed to attach resource to note: ${await response.text()}` + `[Joplin Export] Failed to attach resource to note: ${await response.text()}` ); } } @@ -398,7 +420,7 @@ async function processMail(mailHeader: any) { } // Three ways to export notes: by menu button, hotkey or context menu. -browser.browserAction.onClicked.addListener(getAndProcessMessages); +browser.browserAction.onClicked.addListener(handleMenuButton); messenger.commands.onCommand.addListener(handleHotkey); browser.menus.onClicked.addListener(handleContextMenu); @@ -407,6 +429,7 @@ export { getAndProcessMessages, handleContextMenu, handleHotkey, + handleMenuButton, onlyWhitespace, processMail, renderString, From 6e17bb8f183c43395b2896ea3a305c854e68a8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20D=C3=B6rfelt?= Date: Wed, 29 May 2024 18:51:52 +0200 Subject: [PATCH 2/3] consider selected and displayed messages At a view without the message body, "getDisplayedMessages" returns an empty array, even if messages are selected. To mitigate this, check first for selected messages. If there are none, fall back to the displayed messages. The behavior seems to have changed in recent Thunderbird versions (~115). --- __mocks__/browser.ts | 5 ++- __tests__/test_background.ts | 84 ++++++++++++++++++++++++++++++++---- src/background.ts | 38 +++++++++++++--- 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/__mocks__/browser.ts b/__mocks__/browser.ts index 519c7aa..820f2e5 100644 --- a/__mocks__/browser.ts +++ b/__mocks__/browser.ts @@ -34,12 +34,15 @@ export const browser = { browser.browserAction.icon = icon.path; }, }, + mailTabs: { + getSelectedMessages: jest.fn(async (_tabId) => { messages: [] }), + }, menus: { create: jest.fn(), onClicked: { addListener: jest.fn() }, }, messageDisplay: { - getDisplayedMessages: jest.fn(), + getDisplayedMessages: jest.fn(async (_tabId) => >[]), }, messages: { getFull: jest.fn(async () => { diff --git a/__tests__/test_background.ts b/__tests__/test_background.ts index 5e52399..1cb3de3 100644 --- a/__tests__/test_background.ts +++ b/__tests__/test_background.ts @@ -209,7 +209,7 @@ describe("handle button / hotkey / context menu", () => { expectConsole({ log: 1, warn: 0, - error: ["Failed to create note: Invalid token"], + error: ["[Joplin Export] Failed to create note: Invalid token"], }); }); @@ -277,7 +277,7 @@ describe("handle button / hotkey / context menu", () => { test("export by menu button", async () => { messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); - browser.messageDisplay.getDisplayedMessages.mockReturnValueOnce([ + browser.messageDisplay.getDisplayedMessages.mockResolvedValueOnce([ { id: 1 }, ]); @@ -293,7 +293,7 @@ describe("handle button / hotkey / context menu", () => { test("export by hotkey", async () => { messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); - browser.messageDisplay.getDisplayedMessages.mockReturnValueOnce([ + browser.messageDisplay.getDisplayedMessages.mockResolvedValueOnce([ { id: 1 }, ]); @@ -309,7 +309,7 @@ describe("handle button / hotkey / context menu", () => { test("export by context menu", async () => { messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); - browser.messageDisplay.getDisplayedMessages.mockReturnValueOnce([ + browser.messageDisplay.getDisplayedMessages.mockResolvedValueOnce([ { id: 1 }, ]); @@ -324,6 +324,74 @@ describe("handle button / hotkey / context menu", () => { }); }); +describe("handle displayed and selected mails", () => { + test("one selected mail", async () => { + messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); + browser.mailTabs.getSelectedMessages.mockResolvedValueOnce({ + messages: [{ id: 1 }], + }); + + await getAndProcessMessages({ id: 1 }, {}); + + expect(requests.length).toBe(1); + expectConsole({ + log: 1, + warn: 0, + error: 0, + }); + }); + + test("no selected mail, one displayed mail", async () => { + messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); + browser.messageDisplay.getDisplayedMessages.mockResolvedValueOnce([ + { id: 1 }, + ]); + + await getAndProcessMessages({ id: 1 }, {}); + + expect(requests.length).toBe(1); + expectConsole({ + log: 1, + warn: 0, + error: 0, + }); + }); + + test("error at selected mail, one displayed mail", async () => { + messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); + // This error gets thrown when querying the selected messages at a tab + // where are no messages selected. + browser.mailTabs.getSelectedMessages.mockImplementation(() => { + throw new Error(); + }); + browser.messageDisplay.getDisplayedMessages.mockResolvedValueOnce([ + { id: 1 }, + ]); + + await getAndProcessMessages({ id: 1 }, {}); + + expect(requests.length).toBe(1); + expectConsole({ + log: 1, + warn: 0, + error: 0, + }); + }); + + test("no selectedmail, no displayed mail", async () => { + messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); + + await getAndProcessMessages({ id: 1 }, {}); + + expect(requests.length).toBe(0); + expectConsole({ + log: 0, + warn: 1, + error: 0, + }); + }); +}); + describe("process mail", () => { test("empty header", async () => { const result = await processMail(undefined); @@ -483,8 +551,8 @@ describe("process mail", () => { // Finally check the console output. const message = resultFormat === "text/html" - ? "Sending complete email in HTML format." - : "Sending complete email in plain format."; + ? "[Joplin Export] Sending complete email in HTML format." + : "[Joplin Export] Sending complete email in plain format."; expectConsole({ log: [message], warn: 0, @@ -520,7 +588,7 @@ describe("process mail", () => { }); expectConsole({ - log: ["Sending selection in plain format."], + log: ["[Joplin Export] Sending selection in plain format."], warn: 0, error: 0, }); @@ -709,7 +777,7 @@ describe("process tag", () => { expectConsole({ log: 1, - warn: ['Too many matching tags for "multipleTags": a, b'], + warn: ['[Joplin Export] Too many matching tags for "multipleTags": a, b'], error: 0, }); }); diff --git a/src/background.ts b/src/background.ts index dd483ed..4f4c44d 100644 --- a/src/background.ts +++ b/src/background.ts @@ -101,15 +101,10 @@ async function getAndProcessMessages(tab: { id: number }, info: any) { notificationMessage = "API token missing."; success = false; } else { - const mailHeaders = await browser.messageDisplay.getDisplayedMessages( - tab.id - ); - console.debug( - `[Joplin Export] Got ${mailHeaders.length} emails at tab ${tab.id}.` - ); + const messages = await getMessages(tab.id); // Process the mails and check for success. - const results = await Promise.all(mailHeaders.map(processMail)); + const results = await Promise.all(messages.map(processMail)); for (const error of results) { if (error) { console.error(`[Joplin Export] ${error}`); @@ -145,6 +140,35 @@ async function getAndProcessMessages(tab: { id: number }, info: any) { } } +async function getMessages(tabId: number) { + let messages = []; + try { + // Try to get selected messages when in the main mail tab. + const messageList = await browser.mailTabs.getSelectedMessages(tabId); + messages = messageList.messages; + if (messages.length === 0) { + console.debug( + "[Joplin Export] No selected messages. Try to get displayed messages." + ); + messages = await browser.messageDisplay.getDisplayedMessages(tabId); + } + } catch (error: any) { + // Try to get a displayed message when in message tab. + console.debug( + `[Joplin Export] Error at selected messages (${error.message}). Try to get displayed messages.` + ); + messages = await browser.messageDisplay.getDisplayedMessages(tabId); + } + const logMessage = `[Joplin Export] Got ${messages.length} emails at tab ${tabId}.`; + if (messages.length > 0) { + console.debug(logMessage); + } else { + console.warn(logMessage); + } + + return messages; +} + async function processMail(mailHeader: any) { ////////////////////////////////////////////////// // Mail content From c150705118733450d47910e0baff9d0abf6ae1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20D=C3=B6rfelt?= Date: Thu, 30 May 2024 18:41:03 +0200 Subject: [PATCH 3/3] add documentation --- README.md | 4 ++++ doc/release.md | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/README.md b/README.md index fc763dd..8dadf1f 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,10 @@ What to do when the export failed? ## Changelog +### 0.0.7 + +- Bugfix: Make export working again when mail body is not displayed (). + ### 0.0.6 - Add `author` and `user_created_date` metadata. diff --git a/doc/release.md b/doc/release.md index 06eb59a..b4007b4 100644 --- a/doc/release.md +++ b/doc/release.md @@ -6,3 +6,19 @@ 4. Submit the add-on at . NB: [List of valid Thunderbird versions](https://addons.thunderbird.net/en-US/thunderbird/pages/appversions/) + +## How to test a release + +The automated tests should be successful. Additionally, the following cases should be tested manually: + +Export via: + +- Menu button +- Context menu +- Hotkey + +Tabs: + +- Mail tab with mail body +- Mail tab without mail body +- Mail in a separate tab