From decfc74129e6bb7a1b36b93a1af4c301c85799e5 Mon Sep 17 00:00:00 2001 From: Takashi Hashida Date: Fri, 20 Sep 2024 17:37:14 +0900 Subject: [PATCH] Implement warning dialog --- manifest.xml | 2 +- src/web/app.js | 7 +- src/web/dialog.html | 44 ++++----- src/web/dialog.js | 217 ++++++++++++++++++++++++++++++-------------- 4 files changed, 177 insertions(+), 93 deletions(-) diff --git a/manifest.xml b/manifest.xml index 175120e..7fe2dfb 100644 --- a/manifest.xml +++ b/manifest.xml @@ -55,7 +55,7 @@ - + diff --git a/src/web/app.js b/src/web/app.js index e5f7130..5b9e83d 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -101,10 +101,13 @@ function getAllData(callback) { // eslint-disable-next-line @typescript-eslint/no-unused-vars function onMessageSend(event) { getAllData(function () { - console.debug(data); Office.context.ui.displayDialogAsync( window.location.origin + "/dialog.html", - { asyncContext: event }, + { + asyncContext: event, + height: 60, + width: 60, + }, function (asyncResult) { const dialog = asyncResult.value; dialog.addEventHandler(Office.EventType.DialogMessageReceived, (arg) => { diff --git a/src/web/dialog.html b/src/web/dialog.html index e31703e..73dba98 100644 --- a/src/web/dialog.html +++ b/src/web/dialog.html @@ -3,35 +3,35 @@ Confirmation - + + - + + -
-
-
- 登録済みドメインのメールアドレス +
+ +

登録済みドメインのメールアドレス

+ +
-
-
-
-
- 外部ドメインのメールアドレス -
-
-
-
-
- 添付ファイル/その他の警告 -
-
-
- - + + +

外部ドメインのメールアドレス

+ +
+
+ +

添付ファイル/その他の警告

+ +
+
+ OK + Cancel
\ No newline at end of file diff --git a/src/web/dialog.js b/src/web/dialog.js index beed1f4..868da12 100644 --- a/src/web/dialog.js +++ b/src/web/dialog.js @@ -1,4 +1,68 @@ -let totalCheckboxCount = 0; +function parse(recipient) { + const address = /<([^@]+@[^>]+)>\s*$/.test(recipient) ? RegExp.$1 : recipient; + const domain = address.split('@')[1].toLowerCase(); + return { + recipient, + address, + domain, + }; +} + +class RecipientClassifier { + constructor({ internalDomains } = {}) { + const uniquePatterns = new Set( + (internalDomains || []) + .filter(pattern => !pattern.startsWith('#')) // reject commented out items + .map( + pattern => pattern.toLowerCase() + .replace(/^(-?)@/, '$1') // delete needless "@" from domain only patterns: "@example.com" => "example.com" + .replace(/^(-?)(?![^@]+@)/, '$1*@') // normalize to full address patterns: "foo@example.com" => "foo@example.com", "example.com" => "*@example.com" + ) + ); + const negativeItems = new Set( + [...uniquePatterns] + .filter(pattern => pattern.startsWith('-')) + .map(pattern => pattern.replace(/^-/, '')) + ); + for (const negativeItem of negativeItems) { + uniquePatterns.delete(negativeItem); + uniquePatterns.delete(`-${negativeItem}`); + } + this.$internalPatternsMatcher = new RegExp(`^(${[...uniquePatterns].map(pattern => this.$toRegExpSource(pattern)).join('|')})$`, 'i'); + this.classify = this.classify.bind(this); + } + + $toRegExpSource(source) { + // https://stackoverflow.com/questions/6300183/sanitize-string-of-regex-characters-before-regexp-build + const sanitized = source.replace(/[#-.]|[[-^]|[?|{}]/g, '\\$&'); + + const wildcardAccepted = sanitized.replace(/\\\*/g, '.*').replace(/\\\?/g, '.'); + + return wildcardAccepted; + } + + classify(recipients) { + const internals = new Set(); + const externals = new Set(); + + for (const recipient of recipients) { + const classifiedRecipient = { + ...parse(recipient), + }; + const address = classifiedRecipient.address; + if (this.$internalPatternsMatcher.test(address)) + internals.add(classifiedRecipient); + else + externals.add(classifiedRecipient); + } + + return { + internals: Array.from(internals), + externals: Array.from(externals), + }; + } +} + // eslint-disable-next-line @typescript-eslint/no-unused-vars Office.initialize = function (reason) {}; @@ -8,6 +72,11 @@ Office.onReady(function () { sendStatusToParent("ready"); }); +let counter = 0; +function generateTempId() { + return `fcm_temp_${counter++}_${Date.now()}`; +} + function sendStatusToParent(status) { const messageObject = { status: status }; const jsonMessage = JSON.stringify(messageObject); @@ -25,22 +94,71 @@ function onCancel() { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -function checkboxChanged() { - const checkedCount = $(".check-target:checked").length; - const isAllBoxChecked = checkedCount === totalCheckboxCount; - $("#ok-button").prop("disabled", !isAllBoxChecked); +function checkboxChanged(target_element) { + const checkTargetLength = $("fluent-checkbox.check-target").length; + let checkedTargetLength = $("fluent-checkbox.check-target.checked").length; + // If the target is currently checked, the target is unchecked after this function and vice versa. + const is_currently_checked = $(target_element).hasClass('checked'); + if (is_currently_checked) + { + checkedTargetLength -= 1; + } + else + { + checkedTargetLength += 1; + } + const hasUnchecked = checkTargetLength !== checkedTargetLength; + $("#ok-button").prop("disabled", hasUnchecked); +} + +function appendCheckboxes(target, groupedRecipients) +{ + for (const key in groupedRecipients) + { + const recipients = groupedRecipients[key]; + const idForGroup = generateTempId(); + const idForGroupTitle = generateTempId(); + target.append(` +
+

+ +
`); + //In order to escape special chars, adding values with the text function. + $(`#${idForGroupTitle}`).text(key); + const targetElement = $(`#${idForGroup}`) + for(const recipient of recipients){ + appendCheckbox(targetElement, generateTempId(), recipient.address); + } + } } function appendCheckbox(target, id, value) { - target.append(` -
- - -
`); + target.append(``); //In order to escape special chars, adding values with the text function. - $('label[for="' + id + '"]').text(value); - totalCheckboxCount += 1; + $(`#${id}`).text(value); +} + +function classifyRecipients({ to, cc, bcc, trustedDomains }) { + const classifier = new RecipientClassifier({ + internalDomains: trustedDomains || [], + }); + const classifiedTo = classifier.classify(to); + const classifiedCc = classifier.classify(cc); + const classifiedBcc = classifier.classify(bcc); + console.log('classified results: ', { classifiedTo, classifiedCc, classifiedBcc }); + + return { + internals: new Set([ + ...classifiedTo.internals.map(recipient => ({ ...recipient, type: 'To' })), + ...classifiedCc.internals.map(recipient => ({ ...recipient, type: 'Cc' })), + ...classifiedBcc.internals.map(recipient => ({ ...recipient, type: 'Bcc' })), + ]), + externals: new Set([ + ...classifiedTo.externals.map(recipient => ({ ...recipient, type: 'To' })), + ...classifiedCc.externals.map(recipient => ({ ...recipient, type: 'Cc' })), + ...classifiedBcc.externals.map(recipient => ({ ...recipient, type: 'Bcc' })), + ]), + }; } function onMessageFromParent(arg) { @@ -61,59 +179,22 @@ function onMessageFromParent(arg) { // } console.log(data); - - const trustedRecipients = new Set(); - const untrustedRecipients = new Set(); - //const matchedAttachments = new Set(); - - let recipients = []; - if (data.target.to) { - recipients = recipients.concat(data.target.to.map((_) => _.emailAddress)); - } - if (data.target.cc) { - recipients = recipients.concat(data.target.cc.map((_) => _.emailAddress)); - } - if (data.target.bcc) { - recipients = recipients.concat(data.target.bcc.map((_) => _.emailAddress)); - } - - console.log(recipients); - - if (data.config.trustedDomains) { - for (const recipient of recipients) { - let matched = false; - for (const trustedDomain of data.config.trustedDomains) { - matched = recipient.indexOf(trustedDomain) >= 0; - if (matched) { - break; - } - } - if (matched) { - trustedRecipients.add(recipient); - } else { - untrustedRecipients.add(recipient); - } - } - } - - console.debug(trustedRecipients); - console.debug(untrustedRecipients); - - if (trustedRecipients.size > 0) { - //Make id uniq in this page by adding a trailing number. - //This is a temporary implementation. - let num = 0; - for (const trustedRecipient of trustedRecipients) { - const id = `trusted-${num++}`; - appendCheckbox($("#trusted-domains"), id, trustedRecipient); - } - } - - if (untrustedRecipients.size > 0) { - let num = 0; - for (const untrustedRecipient of untrustedRecipients) { - const id = `untrusted-${num++}`; - appendCheckbox($("#untrusted-domains"), id, untrustedRecipient); - } - } + const to = data.target.to ? + data.target.to.map((_) => _.emailAddress): + []; + let cc = data.target.cc ? + data.target.cc.map((_) => _.emailAddress): + []; + let bcc = data.target.cc ? + data.target.bcc.map((_) => _.emailAddress): + []; + const trustedDomains = data.config.trustedDomains; + + const classifiedRecipients = classifyRecipients({to, cc, bcc, trustedDomains}); + console.log(classifiedRecipients); + + const groupedByTypeInternals = Object.groupBy(classifiedRecipients.internals, item => item.domain); + appendCheckboxes($("#trusted-domains"), groupedByTypeInternals); + const groupedByTypeExternals = Object.groupBy(classifiedRecipients.externals, item => item.domain); + appendCheckboxes($("#untrusted-domains"), groupedByTypeExternals); }