diff --git a/manifest.xml b/manifest.xml index 7c035db3b3ffd3be85680fda0d57aae189488413..319817f5351cdd4e827c72ad1e73d7d018b2df9b 100644 --- a/manifest.xml +++ b/manifest.xml @@ -16,7 +16,7 @@ <Host Name="Mailbox"/> </Hosts> <Requirements> - <Sets> + <Sets DefaultMinVersion="1.8"> <Set Name="Mailbox" MinVersion="1.1"/> </Sets> </Requirements> @@ -28,7 +28,7 @@ </DesktopSettings> </Form> </FormSettings> - <Permissions>ReadWriteItem</Permissions> + <Permissions>ReadWriteMailbox</Permissions> <Rule xsi:type="RuleCollection" Mode="Or"> <Rule xsi:type="ItemIs" ItemType="Message" FormType="Read"/> </Rule> diff --git a/src/taskpane/taskpane.html b/src/taskpane/taskpane.html index 58e8afe02c9e626421b8d20484e75840129de25d..9de56fa9e2d853fc3ac38805412c0dfc26d80154 100644 --- a/src/taskpane/taskpane.html +++ b/src/taskpane/taskpane.html @@ -15,22 +15,24 @@ <p id="messageText"></p> </div> - <!--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> + <span id="selectedFileName" style="margin-left: 10px;">No file selected</span> <div class="worksheet_select"> Worksheet: <select id="worksheetSelect" disabled></select> </div> </fieldset> <fieldset> - <legend>2. E-Mail - Placeholder fields:</legend> + <legend>2. E-Mail - Placeholders fields:</legend> + <div class="recipient_select"> + <label for="recipientSelect">Recipient column:</label> + <select id="recipientSelect" disabled> + <option value="">-</option> + </select> + </div> + <label for="placeholderSelect">Placeholder:</label> <select id="placeholderSelect" disabled></select> <button id="placeholderInsertButton">Insert</button> @@ -64,7 +66,7 @@ <div id="dataPreview"></div> <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> + <!-- script src="taskpane.js"></script--> </div> </body> </html> diff --git a/src/taskpane/taskpane.js b/src/taskpane/taskpane.js index 4ecab959ef08f944c49e25cd1678240cc5a0344e..c8f05b91d1c3bc13987db09310bedaa369e289a5 100644 --- a/src/taskpane/taskpane.js +++ b/src/taskpane/taskpane.js @@ -4,9 +4,9 @@ // Ensure the Office.js library is loaded the office context is fully initialized! -Office.onReady(function (info) { +/*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) { @@ -15,8 +15,8 @@ Office.onReady(function (info) { console.error("Failed to get body type:", result.error); } }); - }*/ -}); + } +});*/ let workbook = null; @@ -30,9 +30,14 @@ let attachmentsFolderHandle = null; // Create a global EventTarget instance const eventBus = new EventTarget(); +eventBus.addEventListener("setSpreadsheetFile", async (event) => { setSpreadsheetFile(event.detail.handle) }); +function debugLog(message) { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${message}`); +} /*** * displayMessage: Warning/Errors in a temporary, non-blocking popup div */ @@ -66,6 +71,42 @@ document.getElementById("closeMessageButton").addEventListener("click", function +function populateColumnSelect(select, columns, noSelection = "") { + if (!select) { + debugLog("populateColumnSelect called with NULL select."); + return; + } + const currentSelectedValue = select.value; + if (noSelection == "") { + select.innerHTML = ""; + } else { + select.innerHTML = `<option value="">${noSelection}</option>`; + } + + columns.forEach((column, index) => { + const option = document.createElement("option"); + option.value = column; + option.textContent = column || `Column ${index + 1}`; + select.appendChild(option); + }); + + // Check if the previously selected value exists in the new list + const matchingOption = Array.from(select.options).find(option => + option.value === currentSelectedValue || option.textContent === currentSelectedValue + ); + + // If the selected value is found, restore it + if (matchingOption) { + select.value = currentSelectedValue; + } else if (columns.length > 0) { + // If no match is found, default to the first option + select.value = columns[0]; + } + select.disabled = false; + select.dispatchEvent(new Event("change")); +} + + /*********************************************************** @@ -79,6 +120,7 @@ const worksheetSelect = document.getElementById("worksheetSelect"); worksheetSelect.addEventListener("change", handleWorksheetSelection); document.getElementById("selectFileButton").addEventListener("click", async () => { +debugLog("called click handler of selectFileButton"); try { // Open the file picker and allow selecting a single file /* if (!('showOpenFilePicker' in window)) { @@ -100,13 +142,10 @@ document.getElementById("selectFileButton").addEventListener("click", async () = multiple: false, }); - const setSpreadsheetEvent = new CustomEvent("setSpreadsheetFile", { - detail: { - handle: handle, // Pass the file handle - }, - }); - eventBus.dispatchEvent(setSpreadsheetEvent); +debugLog("After file picker"); + setSpreadsheetFile(handle); } catch (err) { +debugLog("Catch inside click handler of selectFileButton"); console.error("File selection canceled or failed", err); } }); @@ -117,9 +156,9 @@ document.getElementById("selectFileButton").addEventListener("click", async () = /* * Read the selected file (Excel, csv) into memory */ -eventBus.addEventListener("setSpreadsheetFile", async (event) => { + +async function setSpreadsheetFile(handle) { // Save the file handle for later access - const handle = event.detail.handle; if (!handle) return; const permissionStatus = await handle.queryPermission(); @@ -152,7 +191,7 @@ eventBus.addEventListener("setSpreadsheetFile", async (event) => { // Populate the worksheet dropdown populateWorksheetDropdown(sheetNames); -}); +}; @@ -215,6 +254,8 @@ function handleWorksheetSelection(event) { * ***********************************************************/ +const recipientSelect = document.getElementById("recipientSelect"); + const placeholderSelect = document.getElementById("placeholderSelect"); const placeholderInsertButton = document.getElementById("placeholderInsertButton"); const placeholderCopyLabel = document.getElementById("placeholderCopyLabel"); @@ -223,35 +264,10 @@ const placeholderCopyLabel = document.getElementById("placeholderCopyLabel"); placeholderSelect.addEventListener("change", enablePlaceholderInsert); placeholderInsertButton.addEventListener("click", insertPlaceholder); placeholderCopyLabel.addEventListener("click", copyPlaceholderToClipboard); -eventBus.addEventListener("dataReady", populatePlaceholderSelect); -function populatePlaceholderSelect(event) { - const columns = event.detail.columns; - const currentSelectedValue = placeholderSelect.value; - placeholderSelect.innerHTML = ""; +eventBus.addEventListener("dataReady", (event) => { populateColumnSelect(placeholderSelect, event.detail.columns, ""); }); +eventBus.addEventListener("dataReady", (event) => { populateColumnSelect(recipientSelect, event.detail.columns, "-- (no individual recipients)"); }); - columns.forEach((column, index) => { - const option = document.createElement("option"); - 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")); -} function enablePlaceholderInsert(event) { const placeholder = event.target.value; @@ -261,6 +277,7 @@ function enablePlaceholderInsert(event) { // Function to insert placeholder async function insertPlaceholder() { +debugLog("Called insertPlaceholder"); try { // Get the selected field from the combo box const selectedText = placeholderSelect.options[placeholderSelect.selectedIndex].text; @@ -272,15 +289,19 @@ async function insertPlaceholder() { const placeholder = `{%${selectedText}%}`; +debugLog("Before inserting ${placeholder}"); // Use the Office.js API to get the current item Office.context.mailbox.item.body.getTypeAsync((result) => { if (result.status === Office.AsyncResultStatus.Succeeded) { const bodyType = result.value; + debugLog("Inside async inserting ${placeholder}"); if (bodyType === Office.MailboxEnums.BodyType.Html) { + debugLog("Inside async inserting ${placeholder} to HTML"); return Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { coercionType: Office.CoercionType.Html, }); } else { + debugLog("Inside async inserting ${placeholder} to Text"); return Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { coercionType: Office.CoercionType.Text, }); @@ -291,6 +312,8 @@ async function insertPlaceholder() { console.error("Error inserting placeholder:", error); displayMessage("There was an error inserting the placeholder. Please try again."); } +debugLog("End of insertPlaceholder"); + } @@ -356,40 +379,9 @@ const fileExtensionInput = document.getElementById("fileExtensionInput"); const attachmentFolderButton = document.getElementById("selectAttachmentFolderButton"); const attachmentFolderSpan = document.getElementById("selectedAttachmentFolder"); -eventBus.addEventListener("dataReady", populateAttachmentColumnSelect); +eventBus.addEventListener("dataReady", (event) => { populateColumnSelect(attachmentColumnSelect, event.detail.columns, "No attachment (select column if needed)"); }); 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; - // 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 = 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; }); @@ -584,57 +576,79 @@ async function sendSerialMails(sendNow = false) { const item = Office.context.mailbox.item; const attachmentColumn = attachmentColumnSelect.options[attachmentColumnSelect.selectedIndex].text; const hasAttachmentsConfigured = attachmentColumn && attachmentsFolderHandle; + const recipient = recipientSelect.value || ""; // Explicit column selected for recipient addresses let fileExtension = appendExtensionCheckbox.checked ? fileExtensionInput.value : ""; if (fileExtension && !fileExtension.startsWith(".")) { fileExtension = `.${fileExtension}`; } - + + // Function to read out and return the mail properties that need getAsync... + function readMailPropertyAsync(property) { + if (!property) + return; + return new Promise((resolve, reject) => { + property.getAsync(function(result) { + if (result.status === Office.AsyncResultStatus.Succeeded) { + resolve(result.value); // Resolve with the property value (subject, cc, etc.) + } else { + reject("Failed to get property: " + result.error.message); + } + }); + }); + } + function readMailPropertyOptionsAsync(property, options) { + return new Promise((resolve, reject) => { + property.getAsync(options, function(result) { + if (result.status === Office.AsyncResultStatus.Succeeded) { + resolve(result.value); // Resolve with the property value (subject, cc, etc.) + } else { + reject("Failed to get property: " + result.error.message); + } + }); + }); + } 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"); - } - }); - }); + // recipients (TO, CC, BCC must use fully valid email-Adresses, so we cannot use + // placeholders there -> use the recipientSelect combobox to define a table + // column for individualized recipients per record!) + + // Fetch properties and replace placeholders + const to = await readMailPropertyAsync(item.to); + const cc = await readMailPropertyAsync(item.cc); + const bcc = await readMailPropertyAsync(item.bcc); +// const from = await readMailPropertyAsync(item.from); + const attachments = await readMailPropertyAsync(item.attachments); + const subject = await readMailPropertyAsync(item.subject); + const body = await readMailPropertyOptionsAsync(item.body, Office.CoercionType.Html); + const new_subject = replacePlaceholders(subject, row); + const new_body = replacePlaceholders(body, row); + if (recipient != "") { // add inidividualized recipient + const new_recipient = replacePlaceholders(`{%${recipient}%}`, row); + if (new_recipient != "") { + to.push(new_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); + + // Create a copy of the email + const mailItem = await Office.context.mailbox.displayNewMessageForm({ + subject: new_subject, + htmlBody: new_body, + toRecipients: to, + ccRecipients: cc, + bccRecipients: bcc, + attachments: attachments, + }); + // Setting the sender from the original message is not possible. Outlook will + // always use the one it determines (should be the one selected for the currently + // selected message, but there is no guarantee. It might also be the primary account.) + // Replace fields +// mailItem.subject.setAsync(new_subject, { coercionType: Office.CoercionType.Html }); +// mailItem.body.setAsync(new_body, { coercionType: Office.CoercionType.Html }); - 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) { @@ -681,7 +695,7 @@ async function sendSerialMails(sendNow = false) { * * ***********************************************************/ - +/* worksheetSelect.addEventListener("change", saveFormDataToRoaming); placeholderSelect.addEventListener("change", saveFormDataToRoaming); attachmentColumnSelect.addEventListener("change", saveFormDataToRoaming); @@ -736,21 +750,13 @@ async function openIndexedDB() { }; }); } - +*/ // 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(); - */ - + // Storing/loading handles to/from Office Storage API does not work. // Instead, try storing it to indexDB instead: - const db = await openIndexedDB(); +/* const db = await openIndexedDB(); if (db) { const transaction = db.transaction(['fileHandles'], 'readwrite'); const store = transaction.objectStore('fileHandles'); @@ -761,20 +767,18 @@ async function saveHandles() { }; 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! + // 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); + setSpreadsheetFile(settings.excelFileHandle); } if (settings.attachmentsFolderHandle) { @@ -796,17 +800,20 @@ async function loadFromIndexedDB(storeName) { const result = await request.onsuccess; return result.target.result; } +*/ Office.onReady(function (info) { - if (info.host === Office.HostType.Outlook) { - /*loadHandles().then(() => { + console.log("Office.onReady"); +/* 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(); + }); + loadFormDataFromRoaming(); } + */ });