diff --git a/.eslintrc.json b/.eslintrc.json index e406c09e8b51155d3851935d2ea92cb2a2f6ee89..01faded80601a36803564b31835348860200568e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +1,24 @@ { "plugins": [ - "office-addins" + "office-addins", + "prettier" ], "extends": [ - "plugin:office-addins/recommended" - ] + "plugin:office-addins/recommended", + "plugin:prettier/recommended", + "esling:recommended" + ], + "env": { + "browser": true, + "node": true, + "es2021": true + }, + "globals": { + "Office": "readonly", // Declare Office as a global variable + "XLSX": "readonly" + }, + "rules": { + "no-multiple-empty-lines": "off", // Example: turn off the multiple empty lines rule + "prettier/prettier": "error" // Enforce Prettier formatting as ESLint errors + } } diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..7839c4323d8f48cd428e8805e2f1f29dac11d515 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "auto" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f250de99d4f1bab06a39b408f2e05623be4bc530..730fcf87e6b38bd51cf271ce2bd415d4513ac970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "acorn": "^8.11.3", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^12.0.2", - "eslint-plugin-office-addins": "^3.0.2", + "eslint-plugin-office-addins": "^3.0.3", "file-loader": "^6.2.0", "html-loader": "^5.0.0", "html-webpack-plugin": "^5.6.0", diff --git a/package.json b/package.json index eba23cc140fa66e23b4575897abcef6652de5c7c..79cb0726908d122f2f36041f45044c373c28dbdb 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "acorn": "^8.11.3", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^12.0.2", - "eslint-plugin-office-addins": "^3.0.2", + "eslint-plugin-office-addins": "^3.0.3", "file-loader": "^6.2.0", "html-loader": "^5.0.0", "html-webpack-plugin": "^5.6.0", @@ -59,4 +59,4 @@ "last 2 versions", "ie 11" ] -} \ No newline at end of file +} diff --git a/src/taskpane/taskpane.html b/src/taskpane/taskpane.html index e2f2d4f94c79f28919c08dfd89a7fded1974fbd6..58e8afe02c9e626421b8d20484e75840129de25d 100644 --- a/src/taskpane/taskpane.html +++ b/src/taskpane/taskpane.html @@ -15,12 +15,19 @@ <p id="messageText"></p> </div> - <fieldset> + <!--fieldset> <legend>1. Select data source (Excel file):</legend> <input type="file" id="excelFileInput" accept=".xlsx, .xlsm, .xlsb, .xlts, .xltm, .xls" /> <div class="worksheet_select">Worksheet: <select id="worksheetSelect" disabled></select></div> + </fieldset--> + <fieldset> + <legend>1. Select data source (Excel file):</legend> + <button id="selectFileButton">Select File</button> + <span id="selectedFileName" style="margin-left: 10px; font-slant: italic;">No file selected</span> + <div class="worksheet_select"> + Worksheet: <select id="worksheetSelect" disabled></select> + </div> </fieldset> - <fieldset> <legend>2. E-Mail - Placeholder fields:</legend> @@ -50,9 +57,12 @@ </div> </fieldset> + <button id="sendNowButton" disabled>Send now</button> + <button id="sendLaterButton" disabled>Send later</button> + <div id="dataPreview"></div> - <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> + <script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script> <script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script> <script src="taskpane.js"></script> </div> diff --git a/src/taskpane/taskpane.js b/src/taskpane/taskpane.js index 4603f55ad2368f7d219cda119c448e0dfafde595..4ecab959ef08f944c49e25cd1678240cc5a0344e 100644 --- a/src/taskpane/taskpane.js +++ b/src/taskpane/taskpane.js @@ -2,10 +2,11 @@ * Copyright (c) 2025 Reinhold Kainhofer, Open Tools, office@open-tools.net */ + // Ensure the Office.js library is loaded the office context is fully initialized! Office.onReady(function (info) { // Office is ready - if (info.host === Office.HostType.Outlook) { +/* if (info.host === Office.HostType.Outlook) { // Now you can safely use Office.context.mailbox Office.context.mailbox.item.body.getTypeAsync((result) => { if (result.status === Office.AsyncResultStatus.Succeeded) { @@ -14,18 +15,30 @@ Office.onReady(function (info) { console.error("Failed to get body type:", result.error); } }); - } + }*/ }); + let workbook = null; +let worksheetData = []; // Holds the JSON data from the selected worksheet + +let excelFileHandle = null; +let attachmentsFolderHandle = null; + + + + // Create a global EventTarget instance const eventBus = new EventTarget(); + +/*** + * displayMessage: Warning/Errors in a temporary, non-blocking popup div + */ function displayMessage(message) { const messageDiv = document.getElementById("messageDiv"); const messageText = document.getElementById("messageText"); - const closeButton = document.getElementById("closeMessageButton"); // Set the message text messageText.textContent = message; @@ -40,67 +53,123 @@ function displayMessage(message) { messageDiv.classList.remove("show", "hide"); }, 300); }, 5000); // Hide the message after 5 seconds +} - // Close button functionality - closeButton.addEventListener("click", function() { - messageDiv.classList.add("hide"); - setTimeout(() => { - messageDiv.classList.remove("show", "hide"); - }, 300); // Remove the show and hide classes after the fade-out animation - }); +// Close button functionality +document.getElementById("closeMessageButton").addEventListener("click", function () { + const messageDiv = document.getElementById("messageDiv"); + messageDiv.classList.add("hide"); + setTimeout(() => { + messageDiv.classList.remove("show", "hide"); + }, 300); // Remove the show and hide classes after the fade-out animation +}); -} /*********************************************************** * 1. Select data source (Excel worksheet, CSV file etc.) - * + * * After selecting the file, potentially multiple sheets / tables are available for selection - * + * ***********************************************************/ const worksheetSelect = document.getElementById("worksheetSelect"); - -document.getElementById("excelFileInput").addEventListener("change", handleFile); worksheetSelect.addEventListener("change", handleWorksheetSelection); +document.getElementById("selectFileButton").addEventListener("click", async () => { + try { + // Open the file picker and allow selecting a single file +/* if (!('showOpenFilePicker' in window)) { + displayMessage("File picker API is not supported in this browser."); + return; + }*/ + const [handle] = await window.showOpenFilePicker({ + types: [ + { + description: "Spreadsheet Files", + accept: { + "application/vnd.ms-excel": [".xlsx", ".xlsm", ".xlsb", ".xls"], + "application/vnd.oasis.opendocument.spreadsheet": [".ods"], + "text/csv": [".csv"], + "text/plain": [".txt"], + }, + }, + ], + multiple: false, + }); + + const setSpreadsheetEvent = new CustomEvent("setSpreadsheetFile", { + detail: { + handle: handle, // Pass the file handle + }, + }); + eventBus.dispatchEvent(setSpreadsheetEvent); + } catch (err) { + console.error("File selection canceled or failed", err); + } +}); -/* + + + +/* * Read the selected file (Excel, csv) into memory */ -function handleFile(event) { - const file = event.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = function (e) { - const data = new Uint8Array(e.target.result); - const binary = Array.from(data).map(byte => String.fromCharCode(byte)).join(''); - const fileType = file.name.split(".").pop().toLowerCase(); - - // Determine the file type and parse accordingly - if (fileType === "xlsb") { - workbook = XLSX.read(binary, { type: "binary" }); - } else { - workbook = XLSX.read(data, { type: "array" }); - } +eventBus.addEventListener("setSpreadsheetFile", async (event) => { + // Save the file handle for later access + const handle = event.detail.handle; + if (!handle) return; + + const permissionStatus = await handle.queryPermission(); + if (permissionStatus !== 'granted') { + // Prompt user to grant permission again + await handle.requestPermission(); + console.log('Permission is required for this file!'); + // TODO: exit if permission was not granted! + } + + excelFileHandle = handle; + saveHandles(); + + // Display the file name + document.getElementById("selectedFileName").textContent = handle.name; + + const file = await handle.getFile(); + const fileType = file.name.split('.').pop().toLowerCase(); + const arrayBuffer = await file.arrayBuffer(); + + if (fileType === 'csv' || fileType === 'txt') { + // Convert text content to a SheetJS workbook + const text = await file.text(); + workbook = XLSX.read(text, { type: 'string' }); + } else { + // For binary files (.xlsx, .ods) + workbook = XLSX.read(arrayBuffer, { type: 'array' }); + } + const sheetNames = workbook.SheetNames; + + // Populate the worksheet dropdown + populateWorksheetDropdown(sheetNames); +}); - populateWorksheetDropdown(); - }; - reader.readAsArrayBuffer(file); -} -/* +/* * Make the list of worksheets selectable in a combo box and auto-select the first one */ -function populateWorksheetDropdown() { +function populateWorksheetDropdown(sheetNames) { + if (!sheetNames || sheetNames.length === 0) { + worksheetSelect.disabled = true; + displayMessage("No worksheets found in the workbook.") + console.error("No worksheets found in the workbook."); + return; + } worksheetSelect.innerHTML = ""; worksheetSelect.disabled = false; - workbook.SheetNames.forEach((sheetName) => { + sheetNames.forEach((sheetName) => { const option = document.createElement("option"); option.value = sheetName; option.textContent = sheetName; @@ -108,27 +177,27 @@ function populateWorksheetDropdown() { }); // Automatically select the first worksheet and trigger the "change" event - if (worksheetSelect.options.length >= 1) { + if (worksheetSelect.options.length > 0) { worksheetSelect.selectedIndex = 0; - worksheetSelect.dispatchEvent(new Event("change")); } + worksheetSelect.dispatchEvent(new Event("change")); } function handleWorksheetSelection(event) { const sheetName = event.target.value; - if (!sheetName) return; - if (!workbook) return; // no file loaded yet! + if (!workbook || !sheetName) return;// no file loaded yet or no workseet selected! // Populate the placeholders combobox and the attachments column combo const worksheet = workbook.Sheets[sheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); - const columns = jsonData[0] || []; + worksheetData = jsonData; + const columns = jsonData[0] || []; const dataReadyEvent = new CustomEvent("dataReady", { detail: { - data: jsonData, // Pass the loaded data as part of the event - columns: columns, // Pass the availabe column names - } + data: jsonData, // Pass the loaded data + columns: columns, // Pass the availabe column names + }, }); eventBus.dispatchEvent(dataReadyEvent); } @@ -138,12 +207,12 @@ function handleWorksheetSelection(event) { /*********************************************************** * 2. E-Mail Contents - Allow inserting placeholders from the available column - * + * * After selecting the file and sheet, a combobox with all available placeholders is provided * Next to is is a "Insert" button, which inserts the placeholder of the form {%fieldname%} * into the mail at the current position. For Subject, To/CC/BCC we cannot automatically insert, * but at least provide a copy button to copy the placeholder into the clipboard (TODO!) - * + * ***********************************************************/ const placeholderSelect = document.getElementById("placeholderSelect"); @@ -153,18 +222,33 @@ const placeholderCopyLabel = document.getElementById("placeholderCopyLabel"); // Attach event listener to the previous controls / eventbus placeholderSelect.addEventListener("change", enablePlaceholderInsert); placeholderInsertButton.addEventListener("click", insertPlaceholder); -placeholderCopyLabel.addEventListener("click", copyPlaceholderToClipboard) +placeholderCopyLabel.addEventListener("click", copyPlaceholderToClipboard); eventBus.addEventListener("dataReady", populatePlaceholderSelect); function populatePlaceholderSelect(event) { const columns = event.detail.columns; + const currentSelectedValue = placeholderSelect.value; placeholderSelect.innerHTML = ""; + columns.forEach((column, index) => { const option = document.createElement("option"); - option.value = index; + option.value = column; option.textContent = column || `Column ${index + 1}`; placeholderSelect.appendChild(option); }); + + // Check if the previously selected value exists in the new list + const matchingOption = Array.from(attachmentColumnSelect.options).find(option => + option.value === currentSelectedValue || option.textContent === currentSelectedValue + ); + + // If the selected value is found, restore it + if (matchingOption) { + attachmentColumnSelect.value = currentSelectedValue; + } else if (columns.length > 0) { + // If no match is found, default to the first option + attachmentColumnSelect.value = columns[0]; + } placeholderSelect.disabled = false; placeholderSelect.dispatchEvent(new Event("change")); } @@ -175,12 +259,10 @@ function enablePlaceholderInsert(event) { } - // Function to insert placeholder async function insertPlaceholder() { try { // Get the selected field from the combo box -// const selectedField = placeholderSelect.value; const selectedText = placeholderSelect.options[placeholderSelect.selectedIndex].text; if (!selectedText) { @@ -191,16 +273,17 @@ async function insertPlaceholder() { const placeholder = `{%${selectedText}%}`; // Use the Office.js API to get the current item - await Office.context.mailbox.item.body.getTypeAsync(async (result) => { + Office.context.mailbox.item.body.getTypeAsync((result) => { if (result.status === Office.AsyncResultStatus.Succeeded) { const bodyType = result.value; - if (bodyType === Office.MailboxEnums.BodyType.Html) { - // Insert HTML placeholder into the body - await Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { coercionType: Office.CoercionType.Html } ); - } else if (bodyType === Office.MailboxEnums.BodyType.Text) { - // Insert plain text placeholder into the body - await Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { coercionType: Office.CoercionType.Text } ); + return Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { + coercionType: Office.CoercionType.Html, + }); + } else { + return Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { + coercionType: Office.CoercionType.Text, + }); } } }); @@ -211,12 +294,12 @@ async function insertPlaceholder() { } -/* +/* * Function to insert the placeholder into other fields * -> TODO: Add a dropdown to the "Insert" button to append to Subject/To/CC/BCC alternatively! */ -async function insertIntoField(field, placeholder) { +/*async function insertIntoField(field, placeholder) { try { const currentValue = await Office.context.mailbox.item[field].getAsync(); if (currentValue.status === Office.AsyncResultStatus.Succeeded) { @@ -228,10 +311,10 @@ async function insertIntoField(field, placeholder) { } } - // Example usage: // insertIntoField('subject', '{%fieldname%}'); // insertIntoField('to', '{%fieldname%}'); +*/ @@ -245,12 +328,14 @@ function copyPlaceholderToClipboard() { } const placeholder = `{%${selectedText}%}`; - navigator.clipboard.writeText(placeholder).then(() => { - displayMessage("Copied to clipboard!"); + navigator.clipboard + .writeText(placeholder) + .then(() => { + displayMessage("Copied to clipboard!"); }) .catch((err) => { console.error("Error copying to clipboard:", err); - }); + }); } @@ -258,46 +343,73 @@ function copyPlaceholderToClipboard() { /*********************************************************** * 3. Attachments handling - * + * * Provide a combobox to select the column for individual attachments, - * optionally appends a file extension (e.g. "pdf") and select + * optionally appends a file extension (e.g. "pdf") and select * a folder where the attachments are stored. - * + * ***********************************************************/ const attachmentColumnSelect = document.getElementById("attachmentColumnSelect"); const appendExtensionCheckbox = document.getElementById("appendExtensionCheckbox"); const fileExtensionInput = document.getElementById("fileExtensionInput"); -const attachmentFolderButton = document.getElementById('selectAttachmentFolderButton'); -const attachmentFolderSpan = document.getElementById('selectedAttachmentFolder'); - +const attachmentFolderButton = document.getElementById("selectAttachmentFolderButton"); +const attachmentFolderSpan = document.getElementById("selectedAttachmentFolder"); eventBus.addEventListener("dataReady", populateAttachmentColumnSelect); attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection); - function populateAttachmentColumnSelect(event) { + // (Re-)Populate the column selection combobox. -> Store the current selection and reinstate it after re-filling the list! const columns = event.detail.columns; - attachmentColumnSelect.innerHTML = '<option value="">Select Column</option>'; + // Store the currently selected value + const currentSelectedValue = attachmentColumnSelect.value; + + attachmentColumnSelect.innerHTML = '<option value="">No attachment (select column if needed)</option>'; columns.forEach((column, index) => { const option = document.createElement("option"); - option.value = index; + option.value = column; option.textContent = column || `Column ${index + 1}`; attachmentColumnSelect.appendChild(option); }); + + // Check if the previously selected value exists in the new list + const matchingOption = Array.from(attachmentColumnSelect.options).find(option => + option.value === currentSelectedValue || option.textContent === currentSelectedValue + ); + + // If the selected value is found, restore it + if (matchingOption) { + attachmentColumnSelect.value = currentSelectedValue; + } else if (columns.length > 0) { + // If no match is found, default to the first option + attachmentColumnSelect.value = columns[0]; + } attachmentColumnSelect.disabled = false; + attachmentColumnSelect.dispatchEvent(new Event("change")); } - appendExtensionCheckbox.addEventListener("change", () => { fileExtensionInput.disabled = !appendExtensionCheckbox.checked; }); attachmentFolderButton.addEventListener("click", async () => { try { - // TODO: This does not seem to work with the web-application of Outlook! - const directoryHandle = await window.showDirectoryPicker(); - attachmentFolderSpan.textContent = `Selected folder: ${directoryHandle.name}`; + // If the directory Picker is not supported, show a warning until we find a solution... TODO + if (typeof window.showDirectoryPicker === "undefined") { + console.warn("Directory picker API is not supported in this browser."); + displayMessage("Directory picker API is not supported in this browser."); + return; + } + + const directoryHandle = await window.showDirectoryPicker(); + const setAttachmentFolderEvent = new CustomEvent("setAttachmentsFolder", { + detail: { + handle: directoryHandle, // Pass the file handle + }, + }); + eventBus.dispatchEvent(setAttachmentFolderEvent); + console.log("Selected folder:", directoryHandle); } catch (err) { console.error("Folder selection cancelled or failed", err); @@ -305,6 +417,33 @@ attachmentFolderButton.addEventListener("click", async () => { }); +/* +* Set the selected folder handle as attachments folder: +* - store in global variable for mail sending +* - display name in label +* - store in persistent storage to survive reloads +*/ +eventBus.addEventListener("setAttachmentsFolder", async (event) => { + // Save the file handle for later access + const handle = event.detail.handle; + if (!handle) return; + + const permissionStatus = await handle.queryPermission(); + if (permissionStatus !== 'granted') { + // Prompt user to grant permission again + await handle.requestPermission(); + console.log('Permission is required for this file!'); + // TODO: exit if permission was not granted! + } + + attachmentsFolderHandle = handle; // Update the global variable + saveHandles(); + attachmentFolderSpan.textContent = `Selected folder: ${handle.name}`; +}); + + + + function handleAttachmentColumnSelection() { if (attachmentColumnSelect.value !== "") { @@ -323,9 +462,9 @@ function handleAttachmentColumnSelection() { /*********************************************************** * 4. Sanity Checks, general data handling and review - * - * - * + * + * + * ***********************************************************/ eventBus.addEventListener("dataReady", displayData); @@ -360,11 +499,45 @@ function displayData(event) { /*********************************************************** * 5. Mail sending - * - * - * + * + * + * ***********************************************************/ +const sendNowButton = document.getElementById("sendNowButton"); +const sendLaterButton = document.getElementById("sendLaterButton"); + + +attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection); +eventBus.addEventListener("dataReady", async () => { + sendNowButton.disabled = false; + sendLaterButton.disabled = false; +}); + + + +sendNowButton.addEventListener("click", async () => { + try { + sendSerialMails(/*sendNow=*/ true); + } catch (err) { + console.error("Error sending mails: ", err); + } +}); + +sendLaterButton.addEventListener("click", async () => { + try { + sendSerialMails(/*sendNow=*/ false); + } catch (err) { + console.error("Error sending mails: ", err); + } +}); + + + + + + + function replacePlaceholders(template, row) { return template.replace(/{%([^%]+)%}/g, (match, columnName) => { @@ -372,82 +545,268 @@ function replacePlaceholders(template, row) { }); } -async function addAttachment(mailItem, filePath, fileName) { - const attachmentUrl = `${filePath}/${fileName}`; +async function addAttachment(mailItem, folderHandle, fileName) { try { - await mailItem.addFileAttachmentAsync(attachmentUrl, fileName); + // Step 1: Get the file handle from the directory and file name + const fileHandle = await folderHandle.getFileHandle(fileName); + + // Step 2: Request permission if necessary (query for permission) + const permissionStatus = await fileHandle.queryPermission(); + if (permissionStatus !== 'granted') { + // Request permission from the user if needed + await fileHandle.requestPermission(); + } + + // Step 3: Get the file data (Blob) from the file handle + const file = await fileHandle.getFile(); + const fileBlob = file; // You can use file as a Blob object + + // Step 4: Attach the file to the email + mailItem.attachments.addFileAttachment(fileBlob, file.name) + .then(() => { + console.log(`File "${file.name}" successfully attached to the email.`); + }) + .catch((error) => { + console.error('Error attaching file:', error); + }); } catch (error) { - console.error("Error adding attachment:", error); + console.error('Error accessing or attaching the file:', error); } } -async function sendSerialMails(data, filePath, sendOrSave) { + +async function sendSerialMails(sendNow = false) { + if (!worksheetData || worksheetData.length === 0) { + displayMessage("No data available to send emails. Please select a worksheet."); + return; + } + const item = Office.context.mailbox.item; + const attachmentColumn = attachmentColumnSelect.options[attachmentColumnSelect.selectedIndex].text; + const hasAttachmentsConfigured = attachmentColumn && attachmentsFolderHandle; + + let fileExtension = appendExtensionCheckbox.checked ? fileExtensionInput.value : ""; + if (fileExtension && !fileExtension.startsWith(".")) { + fileExtension = `.${fileExtension}`; + } + + for (const row of worksheetData) { + try { + // Function to get the recipient list as a string (if available) + async function getRecipientAsync(recipientField) { + return new Promise((resolve, reject) => { + recipientField.getAsync((result) => { + if (result.status === Office.AsyncResultStatus.Succeeded) { + // Extract the email addresses (or return empty string if none) + const emails = result.value.map((recipient) => recipient.emailAddress).join(", "); + resolve(emails); // Join multiple recipients with a comma + } else { + reject("Error retrieving recipient"); + } + }); + }); + } + + + // Fetch recipients asynchronously and replace placeholders + const to = await getRecipientAsync(item.to); + const cc = await getRecipientAsync(item.cc); + const bcc = await getRecipientAsync(item.bcc); + const subject = replacePlaceholders(String(item.subject), row); + + + const bodyResult = await item.body.getAsync(Office.CoercionType.Html); + const body = replacePlaceholders(bodyResult.value || "", row); + + // Create a copy of the email + const mailItem = await Office.context.mailbox.item.displayReplyAllFormAsync(); + + // Replace fields + const new_to = replacePlaceholders(to, row); + const new_cc = replacePlaceholders(cc, row); + const new_bcc = replacePlaceholders(bcc, row); + const new_subject = replacePlaceholders(item.subject, row); + if (to != new_to) + mailItem.to = new_to; + if (cc != new_cc) + mailItem.cc = new_cc; + if (bcc != new_bcc) + mailItem.bcc = new_bcc; + if (subject != new_subject) + mailItem.subject = new_subject; + mailItem.body.setAsync(body, { coercionType: Office.CoercionType.Html }); + + // Handle attachment only if configuration and data are available + if (hasAttachmentsConfigured && attachmentColumn) { + const attachmentFileName = row[attachmentColumn]; + if (attachmentFileName) { + let formattedFileName = attachmentFileName.trim(); + + // Ensure the file name has the correct extension + if (fileExtension && !formattedFileName.endsWith(fileExtension)) { + formattedFileName += fileExtension; + } + + try { + await addAttachment(mailItem, attachmentsFolderHandle, formattedFileName); + } catch (error) { + console.warn(`Skipping attachment for row: ${JSON.stringify(row)}, reason: ${error.message}`); + } + } + } + + // Save the mail as a draft or send immediately + if (sendNow) { + await mailItem.sendAsync(); + console.log("Mails sent immediately."); + } else { + await mailItem.saveAsync(); + console.log("Mails saved as draft."); + } - for (const row of data) { - const to = row["To"]; - const cc = row["CC"]; - const bcc = row["BCC"]; - const subject = replacePlaceholders(item.subject, row); - const body = replacePlaceholders(item.body.getAsync(Office.CoercionType.Html).value, row); - const attachmentFile = row["AttachmentFileName"]; - - // Create a copy of the email - const mailItem = await Office.context.mailbox.item.displayReplyAllFormAsync(); - - // Replace fields - mailItem.to = to; - mailItem.cc = cc; - mailItem.bcc = bcc; - mailItem.subject = subject; - mailItem.body.setAsync(body, { coercionType: Office.CoercionType.Html }); - - // Add attachment - if (attachmentFile) { - await addAttachment(mailItem, filePath, attachmentFile); + } catch (error) { + console.error("Error processing row:", JSON.stringify(row), "Error:", error); } + } + displayMessage("Mails sent!"); +} + - // Send the mail - await mailItem.sendAsync(); + + + +/*********************************************************** + * 6. Persisting form settings across sessions: Use Office.js Storage + * + * + * + ***********************************************************/ + +worksheetSelect.addEventListener("change", saveFormDataToRoaming); +placeholderSelect.addEventListener("change", saveFormDataToRoaming); +attachmentColumnSelect.addEventListener("change", saveFormDataToRoaming); +appendExtensionCheckbox.addEventListener("change", saveFormDataToRoaming); +fileExtensionInput.addEventListener("input", saveFormDataToRoaming); +attachmentFolderSpan.addEventListener("click", saveFormDataToRoaming); // TODO: This should be called AFTER the folder picker was used! + +function saveFormDataToRoaming() { + const formData = { + worksheetName: worksheetSelect.value, + placeholder: placeholderSelect.value, + attachmentColumn: attachmentColumnSelect.value, + appendExtension: appendExtensionCheckbox.checked, + fileExtension: fileExtensionInput.value, + }; + Office.context.roamingSettings.set('mailMergeFormData', formData); + Office.context.roamingSettings.saveAsync(); +} + +function loadFormDataFromRoaming() { + const formData = Office.context.roamingSettings.get('mailMergeFormData'); + if (formData) { + worksheetSelect.value = formData.worksheetName || ''; + placeholderSelect.value = formData.placeholder || ''; + attachmentColumnSelect.value = formData.attachmentColumn || ''; + appendExtensionCheckbox.checked = formData.appendExtension || false; + fileExtensionInput.value = formData.fileExtension || ''; + + handleAttachmentColumnSelection(); } } -async function saveSerialMailsAsDrafts(data, filePath) { - const item = Office.context.mailbox.item; - for (const row of data) { - const to = row["To"]; - const cc = row["CC"]; - const bcc = row["BCC"]; - const subject = replacePlaceholders(item.subject, row); - const body = replacePlaceholders(item.body.getAsync(Office.CoercionType.Html).value, row); - const attachmentFile = row["AttachmentFileName"]; - - // Create a new draft mail - const mailItem = await Office.context.mailbox.item.displayReplyAllFormAsync(); - - // Replace fields - mailItem.to = to; - mailItem.cc = cc; - mailItem.bcc = bcc; - mailItem.subject = subject; - mailItem.body.setAsync(body, { coercionType: Office.CoercionType.Html }); - - // Add attachment - if (attachmentFile) { - try { - await addAttachment(mailItem, filePath, attachmentFile); - } catch (error) { - console.warn(`Attachment not found for row: ${JSON.stringify(row)}`); +// Function to open or create the IndexedDB database +async function openIndexedDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open('fileHandlesDB', 1); // Open or create the database + + request.onupgradeneeded = (event) => { + const db = event.target.result; + // Create the object store if it doesn't exist + if (!db.objectStoreNames.contains('fileHandles')) { + db.createObjectStore('fileHandles'); } + }; + + request.onsuccess = (event) => { + resolve(event.target.result); // Return the database instance + }; + + request.onerror = (event) => { + reject('Error opening IndexedDB:', event.target.error); + }; + }); +} + + +// Save file/directory handles persistently +async function saveHandles() { + // Storing/loading handles to/from Office Storage API does not work: +/* const settings = { + excelFileHandle: excelFileHandle ? await excelFileHandle.queryPermission() : null, + attachmentsFolderHandle: attachmentsFolderHandle ? await attachmentsFolderHandle.queryPermission() : null, + }; + Office.context.roamingSettings.set("fileHandles", settings); + Office.context.roamingSettings.saveAsync(); + */ + + // Instead, try storing it to indexDB instead: + const db = await openIndexedDB(); + if (db) { + const transaction = db.transaction(['fileHandles'], 'readwrite'); + const store = transaction.objectStore('fileHandles'); + + const data = { + excelFileHandle: excelFileHandle, + attachmentsFolderHandle: attachmentsFolderHandle, + }; + store.put(data, 'fileHandles'); // Storing the metadata + await transaction.complete; + } +} + +// Load file/directory handles persistently +async function loadHandles() { +// const settings = Office.context.roamingSettings.get("fileHandles"); // Storing/loading handles to/from Office Storage API does not work! + const settings = await loadFromIndexedDB('fileHandles'); // Instead, try loading them from indexDB instead: + + if (settings) { + if (settings.excelFileHandle) { + const setSpreadsheetEvent = new CustomEvent("setSpreadsheetFile", { + detail: { handle: settings.excelFileHandle, }, + }); + eventBus.dispatchEvent(setSpreadsheetEvent); } - // Save the mail as a draft or send immediately - try { - await mailItem.saveAsync(); -// await mailItem.sendAsync(); - console.log("Mail saved as draft."); - } catch (error) { - console.error("Error saving draft:", error); + if (settings.attachmentsFolderHandle) { + const setAttachmentsFolderEvent = new CustomEvent("setAttachmentsFolder", { + detail: { handle: settings.attachmentsFolderHandle, }, + }); + eventBus.dispatchEvent(setAttachmentsFolderEvent); } } } + + +// Example pseudo-function for IndexedDB +async function loadFromIndexedDB(storeName) { + const db = await openIndexedDB(); + const transaction = db.transaction(storeName, 'readonly'); + const store = transaction.objectStore(storeName); + const request = store.get('fileHandles'); + const result = await request.onsuccess; + return result.target.result; +} + +Office.onReady(function (info) { + if (info.host === Office.HostType.Outlook) { + /*loadHandles().then(() => { + if (excelFileHandle) { + console.log("Excel file handle loaded successfully"); + } + if (attachmentsFolderHandle) { + console.log("Attachment folder handle loaded successfully"); + } + });*/ +// loadFormDataFromRoaming(); + } +});