Skip to content

Commit a76dfd1

Browse files
committed
[REF] Outlook: rework logging attachments
Currently logging emails with attachments can encounter several issues, including: - Error if there's a mismatch between the number of img tags and inline attachments - Images will sometimes be embedded in the wrong order - URL images are incorrectly overwritten This commit refactors logRequest to be invariant to the email body and possible missing, extra, and/or out-of-order attachments. This is done by processing the .eml data directly and mapping attachments directly to img tags via contentId. Processing the .eml data is needed since the outlook-js API does not expose a method to fetch the attachment's contentId.
1 parent ef529cd commit a76dfd1

File tree

2 files changed

+79
-92
lines changed

2 files changed

+79
-92
lines changed

outlook/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"fontawesome-4.7": "^4.7.0",
3838
"node-forge": "^0.10.0",
3939
"office-ui-fabric-react": "^7.139.0",
40+
"postal-mime": "^1.1.0",
4041
"react": "^16.8.2",
4142
"react-dom": "^16.8.2",
4243
"react-loader-spinner": "^4.0.0",
@@ -47,7 +48,7 @@
4748
"@babel/core": "^7.11.6",
4849
"@babel/polyfill": "^7.11.5",
4950
"@babel/preset-env": "^7.11.5",
50-
"@types/office-js": "^1.0.138",
51+
"@types/office-js": "^1.0.519",
5152
"@types/office-runtime": "^1.0.17",
5253
"@types/react": "^16.9.49",
5354
"@types/react-dom": "^16.8.4",

outlook/src/taskpane/components/Log/Logger.tsx

Lines changed: 77 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
88
import { faCheck, faEnvelope } from '@fortawesome/free-solid-svg-icons';
99
import { OdooTheme } from '../../../utils/Themes';
1010
import { _t } from '../../../utils/Translator';
11+
import PostalMime from 'postal-mime';
1112

1213
//total attachments size threshold in megabytes
1314
const SIZE_THRESHOLD_TOTAL = 40;
@@ -33,132 +34,117 @@ class Logger extends React.Component<LoggerProps, LoggerState> {
3334
};
3435
}
3536

36-
private fetchAttachmentContent(attachment, index): Promise<any> {
37-
return new Promise<any>((resolve) => {
38-
if (attachment.size > SIZE_THRESHOLD_SINGLE_ELEMENT * 1024 * 1024) {
39-
resolve({
40-
name: attachment.name,
41-
inline: attachment.isInline && attachment.contentType.indexOf('image') >= 0,
42-
oversize: true,
43-
index: index,
44-
});
45-
}
46-
Office.context.mailbox.item.getAttachmentContentAsync(attachment.id, (asyncResult) => {
47-
resolve({
48-
name: attachment.name,
49-
content: asyncResult.value.content,
50-
inline: attachment.isInline && attachment.contentType.indexOf('image') >= 0,
51-
oversize: false,
52-
index: index,
53-
});
54-
});
55-
});
37+
private arrayBufferToBase64(buffer) {
38+
const bytes = new Uint8Array(buffer);
39+
const chunkSize = 0x8000; // 32KB
40+
let binary = '';
41+
42+
for (let i = 0; i < bytes.length; i += chunkSize) {
43+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
44+
}
45+
46+
return btoa(binary);
5647
}
5748

5849
private logRequest = async (event): Promise<any> => {
5950
event.stopPropagation();
6051

6152
this.setState({ logged: 1 });
62-
Office.context.mailbox.item.body.getAsync(Office.CoercionType.Html, async (result) => {
53+
Office.context.mailbox.item.getAsFileAsync(async (result) => {
54+
if (!result.value && result.error) {
55+
this.context.showHttpErrorMessage(result.error);
56+
this.setState({ logged: 0 });
57+
return;
58+
}
59+
60+
const parser = new PostalMime();
61+
const email = await parser.parse(atob(result.value));
62+
const doc = new DOMParser().parseFromString(email.html, 'text/html');
63+
64+
let node: Element = doc.getElementById('appendonsend');
65+
// Remove the history and only log the most recent message.
66+
while (node) {
67+
const next = node.nextElementSibling;
68+
node.parentNode.removeChild(node);
69+
node = next;
70+
}
6371
const msgHeader = `<div>${_t('From : %(email)s', {
64-
email: Office.context.mailbox.item.sender.emailAddress,
72+
email: email.from.address,
6573
})}</div>`;
74+
doc.body.insertAdjacentHTML('afterbegin', msgHeader);
6675
const msgFooter = `<br/><div class="text-muted font-italic">${_t(
6776
'Logged from',
6877
)} <a href="https://www.odoo.com/documentation/master/applications/productivity/mail_plugins.html" target="_blank">${_t(
6978
'Outlook Inbox',
7079
)}</a></div>`;
71-
const body = result.value.split('<div id="x_appendonsend"></div>')[0]; // Remove the history and only log the most recent message.
72-
const message = msgHeader + body + msgFooter;
73-
const doc = new DOMParser().parseFromString(message, 'text/html');
74-
const officeAttachmentDetails = Office.context.mailbox.item.attachments;
75-
let totalSize = 0;
76-
const promises: any[] = [];
77-
const requestJson = {
78-
res_id: this.props.resId,
79-
model: this.props.model,
80-
message: message,
81-
attachments: [],
82-
};
83-
84-
//check if attachment size is bigger then the threshold
85-
officeAttachmentDetails.forEach((officeAttachment) => {
86-
totalSize += officeAttachment.size;
87-
});
80+
doc.body.insertAdjacentHTML('beforeend', msgFooter);
8881

82+
const totalSize = email.attachments.reduce((sum, attachment) => {
83+
return sum + attachment.content.byteLength;
84+
}, 0);
8985
if (totalSize > SIZE_THRESHOLD_TOTAL * 1024 * 1024) {
9086
const warningMessage = _t(
9187
'Warning: Attachments could not be logged in Odoo because their total size' +
9288
' exceeded the allowed maximum.',
9389
{
94-
size: SIZE_THRESHOLD_SINGLE_ELEMENT,
90+
size: SIZE_THRESHOLD_TOTAL,
9591
},
9692
);
9793
doc.body.innerHTML += `<div class="text-danger">${warningMessage}</div>`;
98-
} else {
99-
officeAttachmentDetails.forEach((attachment, index) => {
100-
promises.push(this.fetchAttachmentContent(attachment, index));
101-
});
94+
email.attachments = [];
10295
}
10396

104-
const results = await Promise.all(promises);
105-
106-
let attachments = [];
107-
let oversizeAttachments = [];
108-
let inlineAttachments = [];
109-
110-
results.forEach((result) => {
111-
if (result.inline) {
112-
inlineAttachments[result.index] = result;
97+
const standardAttachments = [];
98+
const oversizedAttachments = [];
99+
const inlineAttachments = {};
100+
for (const attachment of email.attachments) {
101+
if (attachment.disposition === 'inline') {
102+
inlineAttachments[attachment.contentId] = attachment;
103+
} else if (attachment.content.byteLength > SIZE_THRESHOLD_SINGLE_ELEMENT * 1024 * 1024) {
104+
oversizedAttachments.push(attachment.filename);
113105
} else {
114-
if (result.oversize) {
115-
oversizeAttachments.push({
116-
name: result.name,
117-
});
118-
} else {
119-
attachments.push([result.name, result.content]);
120-
}
121-
}
122-
});
123-
// a counter is needed to map img tags with attachments, as outlook does not provide
124-
// an id that enables us to match an img with an attachment.
125-
let j = 0;
126-
const imageElements = doc.getElementsByTagName('img');
127-
128-
inlineAttachments.forEach((inlineAttachment) => {
129-
if (inlineAttachment != null && inlineAttachment.error == undefined) {
130-
if (inlineAttachment.oversize) {
131-
imageElements[j].setAttribute(
132-
'alt',
133-
_t('Could not display image %(attachmentName)s, size is over limit', {
134-
attachmentName: inlineAttachment.name,
135-
}),
136-
);
137-
} else {
138-
const fileExtension = inlineAttachment.name.split('.')[1];
139-
imageElements[j].setAttribute(
140-
'src',
141-
`data:image/${fileExtension};base64, ${inlineAttachment.content}`,
142-
);
143-
}
144-
j++;
106+
standardAttachments.push([attachment.filename, attachment.content]);
145107
}
146-
});
108+
}
147109

148-
if (oversizeAttachments.length > 0) {
149-
const attachmentNames = oversizeAttachments.map((attachment) => `"${attachment.name}"`).join(', ');
110+
if (oversizedAttachments.length > 0) {
150111
const warningMessage = _t(
151112
'Warning: Could not fetch the attachments %(attachments)s as their sizes are bigger then the maximum size of %(size)sMB per each attachment.',
152113
{
153-
attachments: attachmentNames,
154-
size: SIZE_THRESHOLD_TOTAL,
114+
attachments: oversizedAttachments.join(', '),
115+
size: SIZE_THRESHOLD_SINGLE_ELEMENT,
155116
},
156117
);
157118
doc.body.innerHTML += `<div class="text-danger">${warningMessage}</div>`;
158119
}
159120

160-
requestJson.message = doc.body.innerHTML;
161-
requestJson.attachments = attachments;
121+
const imageElements = Array.from(doc.getElementsByTagName('img')).filter((img) =>
122+
img.getAttribute('src')?.startsWith('cid:'),
123+
);
124+
imageElements.forEach((element) => {
125+
const attachment = inlineAttachments[`<${element.src.replace(/^cid:/, '')}>`];
126+
if (attachment?.content.byteLength > SIZE_THRESHOLD_SINGLE_ELEMENT * 1024 * 1024) {
127+
element.setAttribute(
128+
'alt',
129+
_t('Could not display image %(attachmentName)s, size is over limit', {
130+
attachmentName: attachment.filename,
131+
}),
132+
);
133+
} else if (attachment) {
134+
const fileExtension = attachment.filename.split('.')[1];
135+
element.setAttribute(
136+
'src',
137+
`data:image/${fileExtension};base64, ${this.arrayBufferToBase64(attachment.content)}`,
138+
);
139+
}
140+
});
141+
142+
const requestJson = {
143+
res_id: this.props.resId,
144+
model: this.props.model,
145+
message: doc.documentElement.outerHTML,
146+
attachments: standardAttachments,
147+
};
162148

163149
const logRequest = sendHttpRequest(
164150
HttpVerb.POST,

0 commit comments

Comments
 (0)