Skip to content

Commit 9bfde8d

Browse files
committed
[IMP] gmail: improve the email logging
Purpose ======= Before the subject, email from, etc were added in the body of the email when we log it in a record. Now, those values are properly written in the fields of the `mail.message`, and so we need to send them to the Odoo endpoint. Parse all the contacts in the email TO, CC,... to prepare the following commit. Before, if we wait some time before logging the email on a record, an error could be raised because the token we received to get the information expired. To solve that issue, we parse and save then in the state when the addin is loaded. Task-4727609
1 parent 62a4c2d commit 9bfde8d

File tree

6 files changed

+126
-70
lines changed

6 files changed

+126
-70
lines changed

gmail/src/models/email.ts

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ export class Email {
77
accessToken: string;
88
messageId: string;
99
subject: string;
10+
body: string;
11+
timestamp: number;
1012

11-
contactEmail: string;
12-
contactFullEmail: string;
13-
contactName: string;
13+
emailFrom: string;
14+
contacts: EmailContact[];
15+
16+
// When asking for the attachments, a long moment after opening
17+
// the addon, then the token to get the Gmail Message expired
18+
// so we cache the result and ask it when loading the app
19+
_attachmentsParsed: [string[][], ErrorMessage];
1420

1521
constructor(messageId: string = null, accessToken: string = null) {
1622
if (messageId) {
@@ -21,67 +27,86 @@ export class Email {
2127
this.messageId = messageId;
2228
const message = GmailApp.getMessageById(this.messageId);
2329
this.subject = message.getSubject();
24-
25-
const fromHeaders = message.getFrom();
26-
const sent = fromHeaders.toLowerCase().indexOf(userEmail) >= 0;
27-
this.contactFullEmail = sent ? message.getTo() : message.getFrom();
28-
[this.contactName, this.contactEmail] = this._emailSplitTuple(this.contactFullEmail);
30+
this.body = message.getBody();
31+
this.timestamp = message.getDate().getTime();
32+
this.emailFrom = message.getFrom();
33+
34+
this._attachmentsParsed = this.getAttachments();
35+
36+
this.contacts = [
37+
...this._emailSplitTuple(message.getTo(), userEmail),
38+
...this._emailSplitTuple(this.emailFrom, userEmail),
39+
...this._emailSplitTuple(message.getCc(), userEmail),
40+
...this._emailSplitTuple(message.getBcc(), userEmail),
41+
];
2942
}
3043
}
3144

3245
/**
33-
* Ask the email body only if the user asked for it (e.g. asked to log the email).
34-
*/
35-
public get body() {
36-
GmailApp.setCurrentMessageAccessToken(this.accessToken);
37-
const message = GmailApp.getMessageById(this.messageId);
38-
return message.getBody();
39-
}
40-
41-
/**
42-
* Parse a full FROM header and return the name part and the email part.
46+
* Parse a full FROM header and return the name and email parts.
4347
*
4448
* E.G.
45-
* "BOB" <[email protected]> => ["BOB", "[email protected]"]
46-
49+
50+
* => [["BOB", "[email protected]"]]
4751
*
52+
53+
54+
*
55+
56+
* => [
57+
58+
59+
* ]
60+
*
61+
62+
* => [
63+
64+
65+
* ]
66+
*
67+
68+
* => [
69+
70+
71+
* ]
4872
*/
49-
_emailSplitTuple(fullEmail: string): [string, string] {
50-
const match = fullEmail.match(/(.*)<(.*)>/);
51-
fullEmail = fullEmail.replace("<", "").replace(">", "");
52-
53-
if (!match) {
54-
return [fullEmail, fullEmail];
55-
}
56-
57-
const [_, name, email] = match;
58-
59-
if (!name || !email) {
60-
return [fullEmail, fullEmail];
61-
}
73+
_emailSplitTuple(fullEmail: string, userEmail: string): EmailContact[] {
74+
const contacts = [];
75+
const re = /(.*?)<(.*?)>/;
76+
for (const part of fullEmail.split(",")) {
77+
if (part.toLowerCase().indexOf(userEmail) >= 0 || !part.trim()?.length) {
78+
// Skip the user's email
79+
continue;
80+
}
6281

63-
const cleanedName = name.replace(/\"/g, "").trim();
64-
if (!cleanedName || !cleanedName.length) {
65-
return [fullEmail, fullEmail];
82+
const result = part.match(re);
83+
if (!result) {
84+
contacts.push(new EmailContact(part.trim(), part.trim(), part.trim()));
85+
continue;
86+
}
87+
const email = result[2].trim();
88+
let name = result[1].replace(/\"/g, "").trim() || email;
89+
contacts.push(new EmailContact(name, email, part.trim()));
6690
}
67-
68-
return [cleanedName, email];
91+
return contacts;
6992
}
7093

7194
/**
7295
* Unserialize the email object (reverse JSON.stringify).
7396
*/
7497
static fromJson(values: any): Email {
7598
const email = new Email();
76-
7799
email.accessToken = values.accessToken;
78100
email.messageId = values.messageId;
79101
email.subject = values.subject;
80-
81-
email.contactEmail = values.contactEmail;
82-
email.contactFullEmail = values.contactFullEmail;
83-
email.contactName = values.contactName;
84-
102+
email.body = values.body;
103+
email.timestamp = values.timestamp;
104+
email.emailFrom = values.emailFrom;
105+
email.contacts = values.contacts.map((c) => EmailContact.fromJson(c));
106+
email._attachmentsParsed = [
107+
values._attachmentsParsed[0],
108+
ErrorMessage.fromJson(values._attachmentsParsed[1]),
109+
];
85110
return email;
86111
}
87112

@@ -97,6 +122,9 @@ export class Email {
97122
* - Otherwise, the list of attachments base 64 encoded and an empty error message
98123
*/
99124
getAttachments(): [string[][], ErrorMessage] {
125+
if (this._attachmentsParsed) {
126+
return this._attachmentsParsed;
127+
}
100128
GmailApp.setCurrentMessageAccessToken(this.accessToken);
101129
const message = GmailApp.getMessageById(this.messageId);
102130
const gmailAttachments = message.getAttachments();
@@ -124,3 +152,19 @@ export class Email {
124152
return [attachments, new ErrorMessage(null)];
125153
}
126154
}
155+
156+
export class EmailContact {
157+
name: string;
158+
email: string;
159+
fullEmail: string;
160+
161+
constructor(name: string, email: string, fullEmail: string) {
162+
this.name = name;
163+
this.email = email;
164+
this.fullEmail = fullEmail;
165+
}
166+
167+
static fromJson(values: any): EmailContact {
168+
return new EmailContact(values.name, values.email, values.fullEmail);
169+
}
170+
}

gmail/src/models/state.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,15 @@ export class State {
162162
*/
163163
static getLoggingState(messageId: string) {
164164
const cache = CacheService.getUserCache();
165-
const loggingStateStr = cache.get("ODOO_LOGGING_STATE_" + getOdooServerUrl() + "_" + messageId);
165+
const loggingStateStr = cache.get(
166+
"ODOO_LOGGING_STATE_" + getOdooServerUrl() + "_" + messageId,
167+
);
166168

167169
const defaultValues: Record<string, number[]> = {
168-
partners: [],
169-
leads: [],
170-
tickets: [],
171-
tasks: [],
170+
"res.partner": [],
171+
"crm.lead": [],
172+
"helpdesk.ticket": [],
173+
"project.task": [],
172174
};
173175

174176
if (!loggingStateStr || !loggingStateStr.length) {

gmail/src/services/log_email.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ import { getAccessToken } from "./odoo_auth";
1212
*/
1313
function _formatEmailBody(email: Email, error: ErrorMessage): string {
1414
let body = email.body;
15-
16-
body = `<span>${_t("From:")} ${escapeHtml(email.contactEmail)}</span><br/><br/>${body}`;
17-
1815
if (error.code === "attachments_size_exceeded") {
1916
body += `<br/><i>${_t(
2017
"Attachments could not be logged in Odoo because their total size exceeded the allowed maximum.",
@@ -28,8 +25,7 @@ function _formatEmailBody(email: Email, error: ErrorMessage): string {
2825
'class="gmail_chip gmail_drive_chip" style=" min-height: 32px;',
2926
);
3027

31-
body += `<br/><br/>${_t("Logged from")}<b> ${_t("Gmail Inbox")}</b>`;
32-
28+
body += `<br/><br/>${_t("Logged from")}<b> ${_t("Odoo for Gmail")}</b>`;
3329
return body;
3430
}
3531

@@ -40,11 +36,20 @@ export function logEmail(recordId: number, recordModel: string, email: Email): E
4036
const odooAccessToken = getAccessToken();
4137
const [attachments, error] = email.getAttachments();
4238
const body = _formatEmailBody(email, error);
43-
const url = PropertiesService.getUserProperties().getProperty("ODOO_SERVER_URL") + URLS.LOG_EMAIL;
39+
const url =
40+
PropertiesService.getUserProperties().getProperty("ODOO_SERVER_URL") + URLS.LOG_EMAIL;
4441

4542
const response = postJsonRpc(
4643
url,
47-
{ message: body, res_id: recordId, model: recordModel, attachments: attachments },
44+
{
45+
body,
46+
res_id: recordId,
47+
model: recordModel,
48+
attachments: attachments,
49+
email_from: email.emailFrom,
50+
subject: email.subject,
51+
timestamp: email.timestamp,
52+
},
4853
{ Authorization: "Bearer " + odooAccessToken },
4954
);
5055

gmail/src/views/leads.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ import { State } from "../models/state";
1111
function onLogEmailOnLead(state: State, parameters: any) {
1212
const leadId = parameters.leadId;
1313

14-
if (State.checkLoggingState(state.email.messageId, "leads", leadId)) {
15-
state.error = logEmail(leadId, "crm.lead", state.email);
16-
if (!state.error.code) {
17-
State.setLoggingState(state.email.messageId, "leads", leadId);
14+
if (State.checkLoggingState(state.email.messageId, "crm.lead", leadId)) {
15+
const error = logEmail(leadId, "crm.lead", state.email);
16+
if (error.code) {
17+
return notify(error.message);
1818
}
19+
20+
State.setLoggingState(state.email.messageId, "crm.lead", leadId);
1921
return updateCard(buildView(state));
2022
}
21-
return notify(_t("Email already logged on the lead"));
23+
return notify(_t("Email already logged on the opportunity"));
2224
}
2325

2426
function onEmailAlreradyLoggedOnLead(state: State) {
25-
return notify(_t("Email already logged on the lead"));
27+
return notify(_t("Email already logged on the opportunity"));
2628
}
2729

2830
function onCreateLead(state: State) {

gmail/src/views/tasks.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ function onCreateTask(state: State) {
1717
function onLogEmailOnTask(state: State, parameters: any) {
1818
const taskId = parameters.taskId;
1919

20-
if (State.checkLoggingState(state.email.messageId, "tasks", taskId)) {
21-
logEmail(taskId, "project.task", state.email);
22-
if (!state.error.code) {
23-
State.setLoggingState(state.email.messageId, "tasks", taskId);
20+
if (State.checkLoggingState(state.email.messageId, "project.task", taskId)) {
21+
const error = logEmail(taskId, "project.task", state.email);
22+
if (error.code) {
23+
return notify(error.message);
2424
}
25+
State.setLoggingState(state.email.messageId, "project.task", taskId);
2526
return updateCard(buildView(state));
2627
}
2728
return notify(_t("Email already logged on the task"));

gmail/src/views/tickets.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ function onCreateTicket(state: State) {
2828
function onLogEmailOnTicket(state: State, parameters: any) {
2929
const ticketId = parameters.ticketId;
3030

31-
if (State.checkLoggingState(state.email.messageId, "tickets", ticketId)) {
32-
state.error = logEmail(ticketId, "helpdesk.ticket", state.email);
33-
if (!state.error.code) {
34-
State.setLoggingState(state.email.messageId, "tickets", ticketId);
31+
if (State.checkLoggingState(state.email.messageId, "helpdesk.ticket", ticketId)) {
32+
const error = logEmail(ticketId, "helpdesk.ticket", state.email);
33+
if (error.code) {
34+
return notify(error.message);
3535
}
36+
37+
State.setLoggingState(state.email.messageId, "helpdesk.ticket", ticketId);
3638
return updateCard(buildView(state));
3739
}
3840
return notify(_t("Email already logged on the ticket"));

0 commit comments

Comments
 (0)