Skip to content

Commit 079f87a

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 079f87a

File tree

2 files changed

+76
-91
lines changed

2 files changed

+76
-91
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: 74 additions & 90 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,115 @@ 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) => sum + attachment.content.byteLength, 0);
8983
if (totalSize > SIZE_THRESHOLD_TOTAL * 1024 * 1024) {
9084
const warningMessage = _t(
9185
'Warning: Attachments could not be logged in Odoo because their total size' +
9286
' exceeded the allowed maximum.',
9387
{
94-
size: SIZE_THRESHOLD_SINGLE_ELEMENT,
88+
size: SIZE_THRESHOLD_TOTAL,
9589
},
9690
);
9791
doc.body.innerHTML += `<div class="text-danger">${warningMessage}</div>`;
98-
} else {
99-
officeAttachmentDetails.forEach((attachment, index) => {
100-
promises.push(this.fetchAttachmentContent(attachment, index));
101-
});
92+
email.attachments = [];
10293
}
10394

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;
95+
const standardAttachments = [];
96+
const oversizedAttachments = [];
97+
const inlineAttachments = {};
98+
email.attachments.forEach((attachment) => {
99+
if (attachment.disposition === 'inline') {
100+
inlineAttachments[attachment.contentId] = attachment;
101+
} else if (attachment.content.byteLength > SIZE_THRESHOLD_SINGLE_ELEMENT * 1024 * 1024) {
102+
oversizedAttachments.push(attachment.filename);
113103
} 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++;
104+
standardAttachments.push([attachment.filename, attachment.content]);
145105
}
146106
});
147107

148-
if (oversizeAttachments.length > 0) {
149-
const attachmentNames = oversizeAttachments.map((attachment) => `"${attachment.name}"`).join(', ');
108+
if (oversizedAttachments.length > 0) {
150109
const warningMessage = _t(
151110
'Warning: Could not fetch the attachments %(attachments)s as their sizes are bigger then the maximum size of %(size)sMB per each attachment.',
152111
{
153-
attachments: attachmentNames,
154-
size: SIZE_THRESHOLD_TOTAL,
112+
attachments: oversizedAttachments.join(', '),
113+
size: SIZE_THRESHOLD_SINGLE_ELEMENT,
155114
},
156115
);
157116
doc.body.innerHTML += `<div class="text-danger">${warningMessage}</div>`;
158117
}
159118

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

163147
const logRequest = sendHttpRequest(
164148
HttpVerb.POST,

0 commit comments

Comments
 (0)