diff --git a/outlook/package.json b/outlook/package.json index 7fbb2420a..dd76fbb0f 100644 --- a/outlook/package.json +++ b/outlook/package.json @@ -37,6 +37,7 @@ "fontawesome-4.7": "^4.7.0", "node-forge": "^0.10.0", "office-ui-fabric-react": "^7.139.0", + "postal-mime": "^1.1.0", "react": "^16.8.2", "react-dom": "^16.8.2", "react-loader-spinner": "^4.0.0", @@ -47,7 +48,7 @@ "@babel/core": "^7.11.6", "@babel/polyfill": "^7.11.5", "@babel/preset-env": "^7.11.5", - "@types/office-js": "^1.0.138", + "@types/office-js": "^1.0.519", "@types/office-runtime": "^1.0.17", "@types/react": "^16.9.49", "@types/react-dom": "^16.8.4", diff --git a/outlook/src/taskpane/components/Log/Logger.tsx b/outlook/src/taskpane/components/Log/Logger.tsx index c583dc06c..d700b0b8a 100644 --- a/outlook/src/taskpane/components/Log/Logger.tsx +++ b/outlook/src/taskpane/components/Log/Logger.tsx @@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck, faEnvelope } from '@fortawesome/free-solid-svg-icons'; import { OdooTheme } from '../../../utils/Themes'; import { _t } from '../../../utils/Translator'; +import PostalMime from 'postal-mime'; //total attachments size threshold in megabytes const SIZE_THRESHOLD_TOTAL = 40; @@ -33,132 +34,115 @@ class Logger extends React.Component { }; } - private fetchAttachmentContent(attachment, index): Promise { - return new Promise((resolve) => { - if (attachment.size > SIZE_THRESHOLD_SINGLE_ELEMENT * 1024 * 1024) { - resolve({ - name: attachment.name, - inline: attachment.isInline && attachment.contentType.indexOf('image') >= 0, - oversize: true, - index: index, - }); - } - Office.context.mailbox.item.getAttachmentContentAsync(attachment.id, (asyncResult) => { - resolve({ - name: attachment.name, - content: asyncResult.value.content, - inline: attachment.isInline && attachment.contentType.indexOf('image') >= 0, - oversize: false, - index: index, - }); - }); - }); + private arrayBufferToBase64(buffer) { + const bytes = new Uint8Array(buffer); + const chunkSize = 0x8000; // 32KB + let binary = ''; + + for (let i = 0; i < bytes.length; i += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); + } + + return btoa(binary); } private logRequest = async (event): Promise => { event.stopPropagation(); this.setState({ logged: 1 }); - Office.context.mailbox.item.body.getAsync(Office.CoercionType.Html, async (result) => { + Office.context.mailbox.item.getAsFileAsync(async (result) => { + if (!result.value && result.error) { + this.context.showHttpErrorMessage(result.error); + this.setState({ logged: 0 }); + return; + } + + const parser = new PostalMime(); + const email = await parser.parse(atob(result.value)); + const doc = new DOMParser().parseFromString(email.html, 'text/html'); + + let node: Element = doc.getElementById('appendonsend'); + // Remove the history and only log the most recent message. + while (node) { + const next = node.nextElementSibling; + node.parentNode.removeChild(node); + node = next; + } const msgHeader = `
${_t('From : %(email)s', { - email: Office.context.mailbox.item.sender.emailAddress, + email: email.from.address, })}
`; + doc.body.insertAdjacentHTML('afterbegin', msgHeader); const msgFooter = `
${_t( 'Logged from', )} ${_t( 'Outlook Inbox', )}
`; - const body = result.value.split('
')[0]; // Remove the history and only log the most recent message. - const message = msgHeader + body + msgFooter; - const doc = new DOMParser().parseFromString(message, 'text/html'); - const officeAttachmentDetails = Office.context.mailbox.item.attachments; - let totalSize = 0; - const promises: any[] = []; - const requestJson = { - res_id: this.props.resId, - model: this.props.model, - message: message, - attachments: [], - }; - - //check if attachment size is bigger then the threshold - officeAttachmentDetails.forEach((officeAttachment) => { - totalSize += officeAttachment.size; - }); + doc.body.insertAdjacentHTML('beforeend', msgFooter); + const totalSize = email.attachments.reduce((sum, attachment) => sum + attachment.content.byteLength, 0); if (totalSize > SIZE_THRESHOLD_TOTAL * 1024 * 1024) { const warningMessage = _t( 'Warning: Attachments could not be logged in Odoo because their total size' + ' exceeded the allowed maximum.', { - size: SIZE_THRESHOLD_SINGLE_ELEMENT, + size: SIZE_THRESHOLD_TOTAL, }, ); doc.body.innerHTML += `
${warningMessage}
`; - } else { - officeAttachmentDetails.forEach((attachment, index) => { - promises.push(this.fetchAttachmentContent(attachment, index)); - }); + email.attachments = []; } - const results = await Promise.all(promises); - - let attachments = []; - let oversizeAttachments = []; - let inlineAttachments = []; - - results.forEach((result) => { - if (result.inline) { - inlineAttachments[result.index] = result; + const standardAttachments = []; + const oversizedAttachments = []; + const inlineAttachments = {}; + email.attachments.forEach((attachment) => { + if (attachment.disposition === 'inline') { + inlineAttachments[attachment.contentId] = attachment; + } else if (attachment.content.byteLength > SIZE_THRESHOLD_SINGLE_ELEMENT * 1024 * 1024) { + oversizedAttachments.push(attachment.filename); } else { - if (result.oversize) { - oversizeAttachments.push({ - name: result.name, - }); - } else { - attachments.push([result.name, result.content]); - } - } - }); - // a counter is needed to map img tags with attachments, as outlook does not provide - // an id that enables us to match an img with an attachment. - let j = 0; - const imageElements = doc.getElementsByTagName('img'); - - inlineAttachments.forEach((inlineAttachment) => { - if (inlineAttachment != null && inlineAttachment.error == undefined) { - if (inlineAttachment.oversize) { - imageElements[j].setAttribute( - 'alt', - _t('Could not display image %(attachmentName)s, size is over limit', { - attachmentName: inlineAttachment.name, - }), - ); - } else { - const fileExtension = inlineAttachment.name.split('.')[1]; - imageElements[j].setAttribute( - 'src', - `data:image/${fileExtension};base64, ${inlineAttachment.content}`, - ); - } - j++; + standardAttachments.push([attachment.filename, this.arrayBufferToBase64(attachment.content)]); } }); - if (oversizeAttachments.length > 0) { - const attachmentNames = oversizeAttachments.map((attachment) => `"${attachment.name}"`).join(', '); + if (oversizedAttachments.length > 0) { const warningMessage = _t( 'Warning: Could not fetch the attachments %(attachments)s as their sizes are bigger then the maximum size of %(size)sMB per each attachment.', { - attachments: attachmentNames, - size: SIZE_THRESHOLD_TOTAL, + attachments: oversizedAttachments.join(', '), + size: SIZE_THRESHOLD_SINGLE_ELEMENT, }, ); doc.body.innerHTML += `
${warningMessage}
`; } - requestJson.message = doc.body.innerHTML; - requestJson.attachments = attachments; + const imageElements = Array.from(doc.getElementsByTagName('img')).filter((img) => + img.getAttribute('src')?.startsWith('cid:'), + ); + imageElements.forEach((element) => { + const attachment = inlineAttachments[`<${element.src.replace(/^cid:/, '')}>`]; + if (attachment?.content.byteLength > SIZE_THRESHOLD_SINGLE_ELEMENT * 1024 * 1024) { + element.setAttribute( + 'alt', + _t('Could not display image %(attachmentName)s, size is over limit', { + attachmentName: attachment.filename, + }), + ); + } else if (attachment) { + const fileExtension = attachment.filename.split('.')[1]; + element.setAttribute( + 'src', + `data:image/${fileExtension};base64, ${this.arrayBufferToBase64(attachment.content)}`, + ); + } + }); + + const requestJson = { + res_id: this.props.resId, + model: this.props.model, + message: doc.documentElement.outerHTML, + attachments: standardAttachments, + }; const logRequest = sendHttpRequest( HttpVerb.POST,