From 120873c7d66415fe88414c0b92c503abbd45e03a Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 28 Aug 2023 13:34:23 +0200 Subject: [PATCH] Added "View ICS" extension #1178 --- plugins/view-ics/LICENSE | 20 +++++++ plugins/view-ics/index.php | 18 ++++++ plugins/view-ics/message.js | 113 ++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 plugins/view-ics/LICENSE create mode 100644 plugins/view-ics/index.php create mode 100644 plugins/view-ics/message.js diff --git a/plugins/view-ics/LICENSE b/plugins/view-ics/LICENSE new file mode 100644 index 000000000..9e5e56cdd --- /dev/null +++ b/plugins/view-ics/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2022 SnappyMail + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/view-ics/index.php b/plugins/view-ics/index.php new file mode 100644 index 000000000..febaeb875 --- /dev/null +++ b/plugins/view-ics/index.php @@ -0,0 +1,18 @@ +UseLangs(true); + $this->addJs('message.js'); + } +} diff --git a/plugins/view-ics/message.js b/plugins/view-ics/message.js new file mode 100644 index 000000000..cb768402e --- /dev/null +++ b/plugins/view-ics/message.js @@ -0,0 +1,113 @@ +(rl => { + const templateId = 'MailMessageView'; + + addEventListener('rl-view-model.create', e => { + if (templateId === e.detail.viewModelTemplateID) { + + const + template = document.getElementById(templateId), + view = e.detail, + attachmentsPlace = template.content.querySelector('.attachmentsPlace'), + dateRegEx = /(TZID=(?[^:]+):)?(?[0-9]{4})(?[0-9]{2})(?[0-9]{2})T(?[0-9]{2})(?[0-9]{2})(?[0-9]{2})(?Z?)/; + + attachmentsPlace.after(Element.fromHTML(` +
+ + + + + + + + + +
Organizer
Start
End
+
`)); + + view.viewICS = ko.observable(null); + + /** + * TODO + */ + view.message.subscribe(msg => { + view.viewICS(null); + if (msg) { +// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType); + let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType); + if (ics && ics.download) { + // fetch it and parse the VEVENT + rl.fetch(ics.linkDownload()) + .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) + .then(text => { + let VEVENT, + VALARM, + multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE', + 'EXRULE','RSTATUS','RELATED','RESOURCES','RDATE','RRULE'], + lines = text.split(/\r?\n/), + i = lines.length; + while (i--) { + let line = lines[i]; + if (VEVENT) { + while (line.startsWith(' ') && i--) { + line = lines[i] + line.slice(1); + } + if (line.startsWith('END:VALARM')) { + VALARM = {}; + continue; + } else if (line.startsWith('BEGIN:VALARM')) { + VEVENT.VALARM || (VEVENT.VALARM = []); + VEVENT.VALARM.push(VALARM); + VALARM = null; + continue; + } else if (line.startsWith('BEGIN:VEVENT')) { + break; + } + line = line.match(/^([^:;]+)[:;](.+)$/); + if (line) { + if (VALARM) { + VALARM[line[1]] = line[2]; + } else if (multiple.includes(line[1]) || 'X-' == line[1].slice(0,2)) { + VEVENT[line[1]] || (VEVENT[line[1]] = []); + VEVENT[line[1]].push(line[2]); + } else { + if ('DTSTART' === line[1] || 'DTEND' === line[1]) { + let parts = dateRegEx.exec(line[2])?.groups, + options = {dateStyle: 'long', timeStyle: 'short'}; + parts.tz && (options.timeZone = parts.tz); + line[2] = new Date( + parseInt(parts.year, 10), + parseInt(parts.month, 10) - 1, + parseInt(parts.day, 10), + parseInt(parts.hour, 10), + parseInt(parts.minute, 10), + parseInt(parts.second, 10) + ).format(options); + } + VEVENT[line[1]] = line[2]; + } + } + } else if (line.startsWith('END:VEVENT')) { + VEVENT = {}; + } + } +// METHOD:REPLY || METHOD:REQUEST +// console.dir({VEVENT:VEVENT}); + if (VEVENT) { + VEVENT.rawText = text; + VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED'); + VEVENT.isConfirmed = () => VEVENT.STATUS?.includes('CONFIRMED'); + VEVENT.shouldReply = () => VEVENT.METHOD?.includes('REPLY'); + console.dir({ + isCancelled: VEVENT.isCancelled(), + shouldReply: VEVENT.shouldReply() + }); + view.viewICS(VEVENT); + } + }); + } + } + }); + } + }); + +})(window.rl);