Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
O
Outlook MailMerge
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Deploy
Releases
Package registry
Model registry
Operate
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Office
Outlook MailMerge
Commits
52d7c06c
Commit
52d7c06c
authored
6 months ago
by
Reinhold Kainhofer
Browse files
Options
Downloads
Patches
Plain Diff
Implement inserting placeholders, add alert box
parent
530a9be8
No related branches found
No related tags found
No related merge requests found
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
manifest.xml
+1
-1
1 addition, 1 deletion
manifest.xml
src/taskpane/taskpane.css
+39
-1
39 additions, 1 deletion
src/taskpane/taskpane.css
src/taskpane/taskpane.html
+9
-2
9 additions, 2 deletions
src/taskpane/taskpane.html
src/taskpane/taskpane.js
+268
-70
268 additions, 70 deletions
src/taskpane/taskpane.js
with
317 additions
and
74 deletions
manifest.xml
+
1
−
1
View file @
52d7c06c
<?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>
<Version>
1.0
.0.0
</Version>
<ProviderName>
Open Tools
</ProviderName>
<DefaultLocale>
en-US
</DefaultLocale>
<DisplayName
DefaultValue=
"Outlook MailMerge"
/>
...
...
This diff is collapsed.
Click to expand it.
src/taskpane/taskpane.css
+
39
−
1
View file @
52d7c06c
...
...
@@ -28,4 +28,42 @@ body {
padding
:
8px
;
text-align
:
left
;
}
\ No newline at end of file
/* Container for the message */
.message-container
{
display
:
none
;
/* Hidden by default */
position
:
fixed
;
top
:
20px
;
right
:
20px
;
padding
:
15px
;
background-color
:
#f44336
;
/* Red background for error messages */
color
:
white
;
border-radius
:
5px
;
box-shadow
:
0
2px
10px
rgba
(
0
,
0
,
0
,
0.1
);
z-index
:
1000
;
max-width
:
300px
;
font-family
:
Arial
,
sans-serif
;
transition
:
opacity
0.3s
ease
;
}
/* Close button styles */
.close-button
{
position
:
absolute
;
top
:
5px
;
right
:
5px
;
font-size
:
20px
;
cursor
:
pointer
;
color
:
white
;
}
/* Add animation for showing and hiding */
.message-container.show
{
display
:
block
;
opacity
:
1
;
}
.message-container.hide
{
opacity
:
0
;
display
:
none
;
}
\ No newline at end of file
This diff is collapsed.
Click to expand it.
src/taskpane/taskpane.html
+
9
−
2
View file @
52d7c06c
...
...
@@ -10,7 +10,10 @@
<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>
<div
id=
"messageDiv"
class=
"message-container"
>
<span
id=
"closeMessageButton"
class=
"close-button"
>
×
</span>
<p
id=
"messageText"
></p>
</div>
<fieldset>
<legend>
1. Select data source (Excel file):
</legend>
...
...
@@ -21,7 +24,10 @@
<fieldset>
<legend>
2. E-Mail - Placeholder fields:
</legend>
<div
class=
"placeholder_select"
>
Placeholder:
<select
id=
"placeholderSelect"
disabled
></select><input
id=
"insertPlaceholder"
/></div>
<label
for=
"placeholderSelect"
>
Placeholder:
</label>
<select
id=
"placeholderSelect"
disabled
></select>
<button
id=
"placeholderInsertButton"
>
Insert
</button>
<div
id=
"placeholderCopyLabel"
>
Copy
</div>
</fieldset>
<fieldset>
...
...
@@ -47,6 +53,7 @@
<div
id=
"dataPreview"
></div>
<script
src=
"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"
></script>
<script
src=
"https://appsforoffice.microsoft.com/lib/1/hosted/office.js"
></script>
<script
src=
"taskpane.js"
></script>
</div>
</body>
...
...
This diff is collapsed.
Click to expand it.
src/taskpane/taskpane.js
+
268
−
70
View file @
52d7c06c
/*
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
* See LICENSE in the project root for license information.
* Copyright (c) 2025 Reinhold Kainhofer, Open Tools, office@open-tools.net
*/
/* global document, Office */
/*
Office.onReady(() => {
// Office Add-in is ready
console.log("Add-in is ready.");
});
Office.onReady((info) => {
// 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
)
{
document.getElementById("sideload-msg").style.display = "none";
document.getElementById("app-body").style.display = "flex";
document.getElementById("run").onclick = run;
// Now you can safely use Office.context.mailbox
Office
.
context
.
mailbox
.
item
.
body
.
getTypeAsync
((
result
)
=>
{
if
(
result
.
status
===
Office
.
AsyncResultStatus
.
Succeeded
)
{
console
.
log
(
result
.
value
);
// the body type (e.g., Html or Text)
}
else
{
console
.
error
(
"
Failed to get body type:
"
,
result
.
error
);
}
});
}
});
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
;
// Create a global EventTarget instance
const
eventBus
=
new
EventTarget
();
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
;
// Show the message
messageDiv
.
classList
.
add
(
"
show
"
);
messageDiv
.
classList
.
remove
(
"
hide
"
);
setTimeout
(()
=>
{
messageDiv
.
classList
.
add
(
"
hide
"
);
setTimeout
(()
=>
{
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
});
fileInput
.
addEventListener
(
"
change
"
,
handleFile
);
}
/***********************************************************
* 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
);
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
);
}
});
/*
* Read the selected file (Excel, csv) into memory
*/
function
handleFile
(
event
)
{
const
file
=
event
.
target
.
files
[
0
];
if
(
!
file
)
return
;
...
...
@@ -66,26 +77,25 @@ function handleFile(event) {
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
const
options
=
{
type
:
"
array
"
};
if
(
fileType
===
"
xlsb
"
)
{
options
.
type
=
"
binary
"
;
workbook
=
XLSX
.
read
(
binary
,
{
type
:
"
binary
"
});
}
else
{
workbook
=
XLSX
.
read
(
data
,
{
type
:
"
array
"
});
}
workbook
=
XLSX
.
read
(
data
,
options
);
populateWorksheetDropdown
();
};
const
fileType
=
file
.
name
.
split
(
"
.
"
).
pop
().
toLowerCase
();
if
(
fileType
===
"
xlsb
"
)
{
reader
.
readAsBinaryString
(
file
);
}
else
{
reader
.
readAsArrayBuffer
(
file
);
}
reader
.
readAsArrayBuffer
(
file
);
}
/*
* Make the list of worksheets selectable in a combo box and auto-select the first one
*/
function
populateWorksheetDropdown
()
{
worksheetSelect
.
innerHTML
=
""
;
worksheetSelect
.
disabled
=
false
;
...
...
@@ -112,28 +122,190 @@ function handleWorksheetSelection(event) {
// 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
]
||
[];
populateColumnSelect
(
jsonData
[
0
]
||
[]);
displayData
(
jsonData
);
// TODO: Where/how shall we display the table data?
const
dataReadyEvent
=
new
CustomEvent
(
"
dataReady
"
,
{
detail
:
{
data
:
jsonData
,
// Pass the loaded data as part of the event
columns
:
columns
,
// Pass the availabe column names
}
});
eventBus
.
dispatchEvent
(
dataReadyEvent
);
}
function
populateColumnSelect
(
columns
)
{
/***********************************************************
* 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
"
);
const
placeholderInsertButton
=
document
.
getElementById
(
"
placeholderInsertButton
"
);
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
)
eventBus
.
addEventListener
(
"
dataReady
"
,
populatePlaceholderSelect
);
function
populatePlaceholderSelect
(
event
)
{
const
columns
=
event
.
detail
.
columns
;
placeholderSelect
.
innerHTML
=
""
;
columns
.
forEach
((
column
,
index
)
=>
{
const
option
=
document
.
createElement
(
"
option
"
);
option
.
value
=
index
;
option
.
textContent
=
column
||
`Column
${
index
+
1
}
`
;
placeholderSelect
.
appendChild
(
option
);
});
placeholderSelect
.
disabled
=
false
;
placeholderSelect
.
dispatchEvent
(
new
Event
(
"
change
"
));
}
function
enablePlaceholderInsert
(
event
)
{
const
placeholder
=
event
.
target
.
value
;
placeholderInsertButton
.
disabled
=
!
placeholder
;
}
// 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
)
{
displayMessage
(
"
Please select a field before inserting.
"
);
return
;
}
const
placeholder
=
`{%
${
selectedText
}
%}`
;
// Use the Office.js API to get the current item
await
Office
.
context
.
mailbox
.
item
.
body
.
getTypeAsync
(
async
(
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
}
);
}
}
});
}
catch
(
error
)
{
console
.
error
(
"
Error inserting placeholder:
"
,
error
);
displayMessage
(
"
There was an error inserting the placeholder. Please try again.
"
);
}
}
/*
* 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
)
{
try
{
const
currentValue
=
await
Office
.
context
.
mailbox
.
item
[
field
].
getAsync
();
if
(
currentValue
.
status
===
Office
.
AsyncResultStatus
.
Succeeded
)
{
const
updatedValue
=
currentValue
.
value
+
placeholder
;
await
Office
.
context
.
mailbox
.
item
[
field
].
setAsync
(
updatedValue
);
}
}
catch
(
error
)
{
console
.
error
(
`Error inserting into
${
field
}
:`
,
error
);
}
}
// Example usage:
// insertIntoField('subject', '{%fieldname%}');
// insertIntoField('to', '{%fieldname%}');
function
copyPlaceholderToClipboard
()
{
//const selectedField = placeholderSelect.value;
const
selectedText
=
placeholderSelect
.
options
[
placeholderSelect
.
selectedIndex
].
text
;
if
(
!
selectedText
)
{
displayMessage
(
"
Please select a field before copying.
"
);
return
;
}
const
placeholder
=
`{%
${
selectedText
}
%}`
;
navigator
.
clipboard
.
writeText
(
placeholder
).
then
(()
=>
{
displayMessage
(
"
Copied to clipboard!
"
);
})
.
catch
((
err
)
=>
{
console
.
error
(
"
Error copying to clipboard:
"
,
err
);
});
}
/***********************************************************
* 3. Attachments handling
*
* Provide a combobox to select the column for individual attachments,
* 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
'
);
eventBus
.
addEventListener
(
"
dataReady
"
,
populateAttachmentColumnSelect
);
attachmentColumnSelect
.
addEventListener
(
"
change
"
,
handleAttachmentColumnSelection
);
function
populateAttachmentColumnSelect
(
event
)
{
const
columns
=
event
.
detail
.
columns
;
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
}
`
;
option
.
textContent
=
column
||
`
Column
${
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
;
}
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
}
`
;
console
.
log
(
"
Selected folder:
"
,
directoryHandle
);
}
catch
(
err
)
{
console
.
error
(
"
Folder selection cancelled or failed
"
,
err
);
}
});
function
handleAttachmentColumnSelection
()
{
if
(
attachmentColumnSelect
.
value
!==
""
)
{
appendExtensionCheckbox
.
disabled
=
false
;
...
...
@@ -145,7 +317,22 @@ function handleAttachmentColumnSelection() {
}
}
function
displayData
(
data
)
{
/***********************************************************
* 4. Sanity Checks, general data handling and review
*
*
*
***********************************************************/
eventBus
.
addEventListener
(
"
dataReady
"
,
displayData
);
function
displayData
(
event
)
{
const
data
=
event
.
detail
.
data
;
const
previewDiv
=
document
.
getElementById
(
"
dataPreview
"
);
previewDiv
.
innerHTML
=
""
;
...
...
@@ -168,6 +355,17 @@ function displayData(data) {
previewDiv
.
appendChild
(
table
);
}
/***********************************************************
* 5. Mail sending
*
*
*
***********************************************************/
function
replacePlaceholders
(
template
,
row
)
{
return
template
.
replace
(
/{%
([^
%
]
+
)
%}/g
,
(
match
,
columnName
)
=>
{
return
row
[
columnName
]
||
""
;
// Replace with column value or empty string
...
...
@@ -183,7 +381,7 @@ async function addAttachment(mailItem, filePath, fileName) {
}
}
async
function
sendSerialMails
(
data
,
filePath
)
{
async
function
sendSerialMails
(
data
,
filePath
,
sendOrSave
)
{
const
item
=
Office
.
context
.
mailbox
.
item
;
for
(
const
row
of
data
)
{
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment