Skip to content
Snippets Groups Projects
Commit 530a9be8 authored by Reinhold Kainhofer's avatar Reinhold Kainhofer
Browse files

Initial version of the Outlook Mailmerge Plugin

Template generated by yeoman office, adjustements to the manifest.
The JS code and the taskpane layout was partly written with the help of ChatGPT.

Selecting and loading the Excel file works, sending functionality is not there yet (at least not tested)
parents
Branches
No related tags found
No related merge requests found
{
"plugins": [
"office-addins"
],
"extends": [
"plugin:office-addins/recommended"
]
}
node_modules/
dist/
build/
.cache/
.temp/
*.log
.env
Thumbs.db
.vscode/
.idea/
.yo-repository
assets/icon-128.png

4.58 KiB

assets/icon-16.png

1.56 KiB

assets/icon-32.png

2.33 KiB

assets/icon-64.png

2.06 KiB

assets/icon-80.png

4.72 KiB

assets/logo-filled.png

11.6 KiB

{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"esmodules": false
}
}
],
]
}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
<Id>8df42c88-61ce-4c8a-aa94-a46d56cbf46c</Id>
<Version>0.1.0.0</Version>
<ProviderName>Open Tools</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Outlook MailMerge"/>
<Description DefaultValue="Use an Excel table as data source to send out serial Mails with individual attachments for each receipient."/>
<IconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
<HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-128.png"/>
<SupportUrl DefaultValue="https://www.open-tools.net/help"/>
<AppDomains>
<AppDomain>https://www.open-tools.net</AppDomain>
</AppDomains>
<Hosts>
<Host Name="Mailbox"/>
</Hosts>
<Requirements>
<Sets>
<Set Name="Mailbox" MinVersion="1.1"/>
</Sets>
</Requirements>
<FormSettings>
<Form xsi:type="ItemRead">
<DesktopSettings>
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
<RequestedHeight>250</RequestedHeight>
</DesktopSettings>
</Form>
</FormSettings>
<Permissions>ReadWriteItem</Permissions>
<Rule xsi:type="RuleCollection" Mode="Or">
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Read"/>
</Rule>
<DisableEntityHighlighting>false</DisableEntityHighlighting>
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides" xsi:type="VersionOverridesV1_0">
<Requirements>
<bt:Sets DefaultMinVersion="1.3">
<bt:Set Name="Mailbox"/>
</bt:Sets>
</Requirements>
<Hosts>
<Host xsi:type="MailHost">
<DesktopFormFactor>
<FunctionFile resid="Commands.Url"/>
<ExtensionPoint xsi:type="MessageComposeCommandSurface">
<OfficeTab id="TabDefault">
<Group id="msgComposeCmdGroup">
<Label resid="GroupLabel"/>
<Control xsi:type="Button" id="msgComposeMailMerge">
<Label resid="MailMergeButton.Label"/>
<Supertip>
<Title resid="MailMergeButton.Label"/>
<Description resid="MailMergeButton.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ShowTaskpane">
<SourceLocation resid="Taskpane.Url"/>
</Action>
</Control>
</Group>
</OfficeTab>
</ExtensionPoint>
</DesktopFormFactor>
</Host>
</Hosts>
<Resources>
<bt:Images>
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
</bt:Urls>
<bt:ShortStrings>
<bt:String id="GroupLabel" DefaultValue="Mail Merge"/>
<bt:String id="MailMergeButton.Label" DefaultValue="Show MailMerge"/>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="MailMergeButton.Tooltip" DefaultValue="Displays the pane to set up your mail merge (Excel data source, attachments column, send mail settings, etc.)."/>
</bt:LongStrings>
</Resources>
</VersionOverrides>
</OfficeApp>
\ No newline at end of file
This diff is collapsed.
{
"name": "office-addin-taskpane-js",
"version": "0.0.1",
"repository": {
"type": "git",
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane-JS.git"
},
"license": "MIT",
"config": {
"app_to_debug": "outlook",
"app_type_to_debug": "desktop",
"dev_server_port": 3000
},
"scripts": {
"build": "webpack --mode production",
"build:dev": "webpack --mode development",
"dev-server": "webpack serve --mode development",
"lint": "office-addin-lint check",
"lint:fix": "office-addin-lint fix",
"prettier": "office-addin-lint prettier",
"signin": "office-addin-dev-settings m365-account login",
"signout": "office-addin-dev-settings m365-account logout",
"start": "office-addin-debugging start manifest.xml",
"stop": "office-addin-debugging stop manifest.xml",
"validate": "office-addin-manifest validate manifest.xml",
"watch": "webpack --mode development --watch"
},
"dependencies": {
"core-js": "^3.36.0",
"regenerator-runtime": "^0.14.1"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.25.4",
"@types/office-js": "^1.0.377",
"@types/office-runtime": "^1.0.35",
"acorn": "^8.11.3",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"eslint-plugin-office-addins": "^3.0.2",
"file-loader": "^6.2.0",
"html-loader": "^5.0.0",
"html-webpack-plugin": "^5.6.0",
"office-addin-cli": "^1.6.5",
"office-addin-debugging": "^5.1.6",
"office-addin-dev-certs": "^1.13.5",
"office-addin-lint": "^2.3.5",
"office-addin-manifest": "^1.13.6",
"office-addin-prettier-config": "^1.2.1",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
"source-map-loader": "^5.0.0",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "5.1.0"
},
"prettier": "office-addin-prettier-config",
"browserslist": [
"last 2 versions",
"ie 11"
]
}
\ No newline at end of file
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<!-- Office JavaScript API -->
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js"></script>
</head>
<body>
</body>
</html>
\ No newline at end of file
/*
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
* See LICENSE in the project root for license information.
*/
/* global Office */
Office.onReady(() => {
// If needed, Office.js is ready to be called.
});
/**
* Shows a notification when the add-in command is executed.
* @param event {Office.AddinCommands.Event}
*/
function action(event) {
const message = {
type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
message: "Performed action.",
icon: "Icon.80x80",
persistent: true,
};
// Show a notification message.
Office.context.mailbox.item.notificationMessages.replaceAsync("action", message);
// Be sure to indicate when the add-in command function is complete.
event.completed();
}
// Register the function with Office.
Office.actions.associate("action", action);
body {
font-family: Arial, sans-serif;
margin: 20px;
}
h1 {
color: #0078D4;
}
#dataPreview {
margin-top: 20px;
border: 1px solid #ccc;
padding: 10px;
max-height: 300px;
overflow-y: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
table, th, td {
border: 1px solid #ccc;
}
th, td {
padding: 8px;
text-align: left;
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Data Importer</title>
<link rel="stylesheet" href="taskpane.css">
</head>
<body>
<div name="background">
<p>This add-on provides serial mail with individual attachments. Data is provided by an Excel table, Individual fields in the text can be inserted as {%fieldname%}.</p>
<p class="label"></p>
<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>2. E-Mail - Placeholder fields:</legend>
<div class="placeholder_select">Placeholder: <select id="placeholderSelect" disabled></select><input id="insertPlaceholder" /></div>
</fieldset>
<fieldset>
<legend>3. Attachments:</legend>
<div class="attachment_select">
<label for="attachmentColumnSelect">Attachment column:</label>
<select id="attachmentColumnSelect" disabled>
<option value="">Attachment column</option>
</select>
</div>
<div class="fielextension_input">
<input type="checkbox" id="appendExtensionCheckbox" disabled>
<label for="appendExtensionCheckbox" disabled>Append file extension:</label>
<input type="text" id="fileExtensionInput" placeholder="pdf" disabled>
</div>
<div class="attachmentpath">
<button id="selectAttachmentFolderButton">Select Folder</button>
<span id="selectedAttachmentFolder"></span>
</div>
</fieldset>
<div id="dataPreview"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="taskpane.js"></script>
</div>
</body>
</html>
/*
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
* See LICENSE in the project root for license information.
*/
/* global document, Office */
/*
Office.onReady(() => {
// Office Add-in is ready
console.log("Add-in is ready.");
});
Office.onReady((info) => {
if (info.host === Office.HostType.Outlook) {
document.getElementById("sideload-msg").style.display = "none";
document.getElementById("app-body").style.display = "flex";
document.getElementById("run").onclick = run;
}
});
export async function run() {
// Insert your Outlook code here
const item = Office.context.mailbox.item;
let insertAt = document.getElementById("item-subject");
let label = document.createElement("b").appendChild(document.createTextNode("Subject: "));
insertAt.appendChild(label);
insertAt.appendChild(document.createElement("br"));
insertAt.appendChild(document.createTextNode(item.subject));
insertAt.appendChild(document.createElement("br"));
};
*/
const fileInput = document.getElementById("excelFileInput");
const worksheetSelect = document.getElementById("worksheetSelect");
const placeholderSelect = document.getElementById("placeholderSelect");
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');
let workbook = null;
fileInput.addEventListener("change", handleFile);
worksheetSelect.addEventListener("change", handleWorksheetSelection);
attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection);
appendExtensionCheckbox.addEventListener("change", () => {
fileExtensionInput.disabled = !appendExtensionCheckbox.checked;
});
attachmentFolderButton.addEventListener("click", async () => {
try {
const directoryHandle = await window.showDirectoryPicker();
attachmentFolderSpan.textContent = `Selected folder: ${directoryHandle.name}`;
console.log("Selected folder:", directoryHandle);
} catch (err) {
console.error("Folder selection cancelled or failed", err);
}
});
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 fileType = file.name.split(".").pop().toLowerCase();
// Determine the file type and parse accordingly
const options = { type: "array" };
if (fileType === "xlsb") {
options.type = "binary";
}
workbook = XLSX.read(data, options);
populateWorksheetDropdown();
};
const fileType = file.name.split(".").pop().toLowerCase();
if (fileType === "xlsb") {
reader.readAsBinaryString(file);
} else {
reader.readAsArrayBuffer(file);
}
}
function populateWorksheetDropdown() {
worksheetSelect.innerHTML = "";
worksheetSelect.disabled = false;
workbook.SheetNames.forEach((sheetName) => {
const option = document.createElement("option");
option.value = sheetName;
option.textContent = sheetName;
worksheetSelect.appendChild(option);
});
// Automatically select the first worksheet and trigger the "change" event
if (worksheetSelect.options.length >= 1) {
worksheetSelect.selectedIndex = 0;
worksheetSelect.dispatchEvent(new Event("change"));
}
}
function handleWorksheetSelection(event) {
const sheetName = event.target.value;
if (!sheetName) return;
if (!workbook) return; // no file loaded yet!
// Populate the placeholders combobox and the attachments column combo
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
populateColumnSelect(jsonData[0] || []);
displayData(jsonData); // TODO: Where/how shall we display the table data?
}
function populateColumnSelect(columns) {
placeholderSelect.innerHTML = "";
attachmentColumnSelect.innerHTML = '<option value="">Select Column</option>';
columns.forEach((column, index) => {
const option = document.createElement("option");
option.value = index;
option.textContent = column || `Spalte ${index + 1}`;
attachmentColumnSelect.appendChild(option);
// Insert the same option into the placeholder combobox:
const option2 = option.cloneNode(true);
placeholderSelect.appendChild(option2);
});
attachmentColumnSelect.disabled = false;
placeholderSelect.disabled = false;
}
function handleAttachmentColumnSelection() {
if (attachmentColumnSelect.value !== "") {
appendExtensionCheckbox.disabled = false;
attachmentFolderButton.disabled = false;
} else {
appendExtensionCheckbox.disabled = true;
fileExtensionInput.disabled = true;
attachmentFolderButton.disabled = true;
}
}
function displayData(data) {
const previewDiv = document.getElementById("dataPreview");
previewDiv.innerHTML = "";
if (data.length === 0) {
previewDiv.textContent = "No data found in the selected worksheet.";
return;
}
const table = document.createElement("table");
data.forEach((row) => {
const tr = document.createElement("tr");
row.forEach((cell) => {
const td = document.createElement("td");
td.textContent = cell;
tr.appendChild(td);
});
table.appendChild(tr);
});
previewDiv.appendChild(table);
}
function replacePlaceholders(template, row) {
return template.replace(/{%([^%]+)%}/g, (match, columnName) => {
return row[columnName] || ""; // Replace with column value or empty string
});
}
async function addAttachment(mailItem, filePath, fileName) {
const attachmentUrl = `${filePath}/${fileName}`;
try {
await mailItem.addFileAttachmentAsync(attachmentUrl, fileName);
} catch (error) {
console.error("Error adding attachment:", error);
}
}
async function sendSerialMails(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 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);
}
// Send the mail
await mailItem.sendAsync();
}
}
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)}`);
}
}
// 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);
}
}
}
/* eslint-disable no-undef */
const devCerts = require("office-addin-dev-certs");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const urlDev = "https://localhost:3000/";
const urlProd = "https://www.open-tools.net/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION
async function getHttpsOptions() {
const httpsOptions = await devCerts.getHttpsServerOptions();
return { ca: httpsOptions.ca, key: httpsOptions.key, cert: httpsOptions.cert };
}
module.exports = async (env, options) => {
const dev = options.mode === "development";
const config = {
devtool: "source-map",
entry: {
polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
taskpane: ["./src/taskpane/taskpane.js", "./src/taskpane/taskpane.html"],
commands: "./src/commands/commands.js",
},
output: {
clean: true,
},
resolve: {
extensions: [".html", ".js"],
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.html$/,
exclude: /node_modules/,
use: "html-loader",
},
{
test: /\.(png|jpg|jpeg|gif|ico)$/,
type: "asset/resource",
generator: {
filename: "assets/[name][ext][query]",
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
filename: "taskpane.html",
template: "./src/taskpane/taskpane.html",
chunks: ["polyfill", "taskpane"],
}),
new CopyWebpackPlugin({
patterns: [
{
from: "assets/*",
to: "assets/[name][ext][query]",
},
{
from: "manifest*.xml",
to: "[name]" + "[ext]",
transform(content) {
if (dev) {
return content;
} else {
return content.toString().replace(new RegExp(urlDev, "g"), urlProd);
}
},
},
],
}),
new HtmlWebpackPlugin({
filename: "commands.html",
template: "./src/commands/commands.html",
chunks: ["polyfill", "commands"],
}),
],
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
server: {
type: "https",
options: env.WEBPACK_BUILD || options.https !== undefined ? options.https : await getHttpsOptions(),
},
port: process.env.npm_package_config_dev_server_port || 3000,
},
};
return config;
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment