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/__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 7470376..1cb3de3 100644 --- a/__tests__/test_background.ts +++ b/__tests__/test_background.ts @@ -12,6 +12,7 @@ import { getAndProcessMessages, handleContextMenu, handleHotkey, + handleMenuButton, onlyWhitespace, processMail, renderString, @@ -208,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"], }); }); @@ -274,9 +275,25 @@ describe("handle button / hotkey / context menu", () => { } ); + test("export by menu button", async () => { + messenger.tabs.query.mockResolvedValueOnce([{ id: 1 }]); + browser.messageDisplay.getDisplayedMessages.mockResolvedValueOnce([ + { 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([ + browser.messageDisplay.getDisplayedMessages.mockResolvedValueOnce([ { id: 1 }, ]); @@ -292,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 }, ]); @@ -307,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); @@ -466,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, @@ -503,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, }); @@ -692,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/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 diff --git a/src/background.ts b/src/background.ts index 57f1c59..4f4c44d 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) { @@ -90,15 +101,13 @@ async function getAndProcessMessages(tab: { id: number }, info: any) { notificationMessage = "API token missing."; success = false; } else { - const mailHeaders = await browser.messageDisplay.getDisplayedMessages( - 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(error); + console.error(`[Joplin Export] ${error}`); success = false; } } @@ -131,12 +140,41 @@ 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 ////////////////////////////////////////////////// - // 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 +266,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 +336,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 +354,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 +369,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 +382,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 +418,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 +436,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 +444,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 +453,7 @@ export { getAndProcessMessages, handleContextMenu, handleHotkey, + handleMenuButton, onlyWhitespace, processMail, renderString,