(rl => { window.identiconSvg = (hash, txt, font) => { // color defaults to last 7 chars as hue at 70% saturation, 50% brightness // hsl2rgb adapted from: https://gist.github.com/aemkei/1325937 let h = (parseInt(hash.substr(-7), 16) / 0xfffffff) * 6, s = 0.7, l = 0.5, v = [ l += s *= l < .5 ? l : 1 - l, l - h % 1 * s * 2, l -= s *= 2, l, l + h % 1 * s, l + s ], m = txt ? 128 : 200, color = 'rgb(' + [ v[ ~~h % 6 ] * m, // red v[ (h | 16) % 6 ] * m, // green v[ (h | 8) % 6 ] * m // blue ].map(Math.round).join(',') + ')'; if (txt) { return ` ${txt} `; } return ` `; }; const size = 50, getEl = id => document.getElementById(id), queue = [], avatars = new Map, ncAvatars = new Map, templateId = 'MailMessageView', getBimiSelector = msg => { // Get 's' value out of 'v=BIMI1; s=foo;' let bimiSelector = msg.headers().valueByName('BIMI-Selector'); bimiSelector = bimiSelector ? bimiSelector.match(/;.*s=([^\s;]+)/)[1] : ''; return bimiSelector || ''; }, getBimiId = msg => ('pass' == msg.from[0].dkimStatus ? 1 : 0) + '-' + getBimiSelector(msg), getAvatarUrl = msg => `?Avatar/${getBimiId(msg)}/${msg.avatar}`, getAvatarUid = msg => `${getBimiId(msg)}/${msg.from[0].email.toLowerCase()}`, getAvatar = msg => ncAvatars.get(msg.from[0].email.toLowerCase()) || avatars.get(getAvatarUid(msg)), hash = async txt => { if (/^[0-9a-f]{15,}$/i.test(txt)) { return txt; } const hashArray = Array.from(new Uint8Array( // await crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(txt.toLowerCase())) await crypto.subtle.digest('SHA-1', (new TextEncoder()).encode(txt.toLowerCase())) )); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string }, fromChars = from => // (from.name?.split(/[^\p{Lu}]+/gu) || []).reduce((a, s) => a + (s || '')), '') (from.name?.split(/[^\p{L}]+/gu) || []).reduce((a, s) => a + (s[0] || ''), '') .slice(0,2) .toUpperCase(), setIdenticon = (from, fn) => hash(from.email).then(hash => fn('data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(window.identiconSvg( hash, fromChars(from), window.getComputedStyle(getEl('rl-app'), null).getPropertyValue('font-family') ))))) ), addQueue = (msg, fn) => { msg.from?.[0] && setIdenticon(msg.from[0], fn); if (rl.pluginSettingsGet('avatars', 'delay')) { queue.push([msg, fn]); runQueue(); } }, runQueue = (() => { let item = queue.shift(); while (item) { if (item[0].from) { let url = getAvatar(item[0]), uid = getAvatarUid(item[0]); if (url) { item[1](url); item = queue.shift(); continue; } else if (!avatars.has(uid)) { let from = item[0].from[0]; rl.pluginRemoteRequest((iError, data) => { if (!iError && data?.Result.type) { url = `data:${data.Result.type};base64,${data.Result.data}`; avatars.set(uid, url); item[1](url); } else { avatars.set(uid, ''); } runQueue(); }, 'Avatar', { bimi: 'pass' == from.dkimStatus ? 1 : 0, bimiSelector: getBimiSelector(item[0]), email: from.email }); break; } } runQueue(); break; } }).debounce(1000); /** * Loads images from Nextcloud contacts */ addEventListener('DOMContentLoaded', () => { // rl.pluginSettingsGet('avatars', 'nextcloud'); if (parent.OC) { const OC = () => parent.OC, nsDAV = 'DAV:', nsNC = 'http://nextcloud.com/ns', nsCard = 'urn:ietf:params:xml:ns:carddav', getElementsByTagName = (parent, namespace, localName) => parent.getElementsByTagNameNS(namespace, localName), getElementValue = (parent, namespace, localName) => getElementsByTagName(parent, namespace, localName)?.item(0)?.textContent, generateUrl = path => OC().webroot + '/remote.php' + path; if (OC().requestToken) { fetch(generateUrl(`/dav/addressbooks/users/${OC().currentUser}/contacts/`), { mode: 'same-origin', cache: 'no-cache', redirect: 'error', credentials: 'same-origin', method: 'REPORT', headers: { requesttoken: OC().requestToken, 'Content-Type': 'application/xml; charset=utf-8', Depth: 1 }, body: '' }) .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) .then(text => { const xmlParser = new DOMParser(), responseList = getElementsByTagName( xmlParser.parseFromString(text, 'application/xml').documentElement, nsDAV, 'response'); for (let i = 0; i < responseList.length; ++i) { const item = responseList.item(i); if (1 == getElementValue(item, nsNC, 'has-photo')) { [...getElementValue(item, nsCard, 'address-data').matchAll(/EMAIL.*?:([^@\r\n]+@[^@\r\n]+)/g)] .forEach(match => { ncAvatars.set( match[1].toLowerCase(), getElementValue(item, nsDAV, 'href') + '?photo' ); }); } } }); } } }); ko.bindingHandlers.fromPic = { init: (element, self, dummy, msg) => { try { if (msg?.from?.[0]) { let url = getAvatar(msg), from = msg.from[0], fn = url=>{element.src = url}; if (url) { fn(url); } else if (msg.avatar) { if (msg.avatar?.startsWith('data:')) { fn(msg.avatar); } else { element.onerror = () => setIdenticon(from, fn); fn(getAvatarUrl(msg)); } } else { addQueue(msg, fn); } } } catch (e) { console.error(e); } } }; addEventListener('rl-view-model.create', e => { if (templateId === e.detail.viewModelTemplateID) { const template = getEl(templateId), messageItemHeader = template.content.querySelector('.messageItemHeader'); if (messageItemHeader) { messageItemHeader.prepend(Element.fromHTML( `` )); } let view = e.detail; view.viewUserPic = ko.observable(''); view.viewUserPicVisible = ko.observable(false); view.message.subscribe(msg => { view.viewUserPicVisible(false); if (msg) { let url = msg.from?.[0] ? getAvatar(msg) : 0, fn = url => { view.viewUserPic(url); view.viewUserPicVisible(true); }; if (url) { fn(url); } else if (msg.avatar) { fn(msg.avatar.startsWith('data:') ? msg.avatar : getAvatarUrl(msg)); } else { // let from = msg.from[0]; // view.viewUserPic(`?Avatar/${'pass' == from.dkimStatus ? 1 : 0}/${encodeURIComponent(from.email)}`); // view.viewUserPicVisible(true); addQueue(msg, fn); } } }); } if ('MailMessageList' === e.detail.viewModelTemplateID) { getEl('MailMessageList').content.querySelectorAll('.messageCheckbox') .forEach(el => el.append(Element.fromHTML(``))); } }); })(window.rl);