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
b7b15d75
Commit
b7b15d75
authored
6 months ago
by
Reinhold Kainhofer
Browse files
Options
Downloads
Patches
Plain Diff
Some more work, fighting with new mail generation..
parent
84f58e7c
No related branches found
No related tags found
No related merge requests found
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
manifest.xml
+2
-2
2 additions, 2 deletions
manifest.xml
src/taskpane/taskpane.html
+10
-8
10 additions, 8 deletions
src/taskpane/taskpane.html
src/taskpane/taskpane.js
+141
-134
141 additions, 134 deletions
src/taskpane/taskpane.js
with
153 additions
and
144 deletions
manifest.xml
+
2
−
2
View file @
b7b15d75
...
...
@@ -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>
ReadWrite
Item
</Permissions>
<Permissions>
ReadWrite
Mailbox
</Permissions>
<Rule
xsi:type=
"RuleCollection"
Mode=
"Or"
>
<Rule
xsi:type=
"ItemIs"
ItemType=
"Message"
FormType=
"Read"
/>
</Rule>
...
...
This diff is collapsed.
Click to expand it.
src/taskpane/taskpane.html
+
10
−
8
View file @
b7b15d75
...
...
@@ -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>
This diff is collapsed.
Click to expand it.
src/taskpane/taskpane.js
+
141
−
134
View file @
b7b15d75
...
...
@@ -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();
}
*/
});
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