#12336 adding gmail email sync error handling (#12383)

I believe that some emails with invalid characters are breaking the sync
process.

this PR attempts to create a "safeParseAddress" function. Hopefully this
will change current behavior of a single email breaking the entire sync
process to the sync process "skipping" an invalid email address and
continuing on.

I opened this because of issues explained in #12336

---------

Co-authored-by: guillim <guigloo@msn.com>
This commit is contained in:
Matt Dvertola
2025-06-03 05:17:48 -04:00
committed by GitHub
parent 7617dd76aa
commit f60b704feb
7 changed files with 141 additions and 61 deletions

View File

@ -40,11 +40,22 @@ export const parseAndFormatGmailMessage = (
return null; return null;
} }
const toParticipants = to ?? deliveredTo;
const participants = [ const participants = [
...formatAddressObjectAsParticipants(from, 'from'), ...(from
...formatAddressObjectAsParticipants(to ?? deliveredTo, 'to'), ? formatAddressObjectAsParticipants([{ address: from }], 'from')
...formatAddressObjectAsParticipants(cc, 'cc'), : []),
...formatAddressObjectAsParticipants(bcc, 'bcc'), ...(toParticipants
? formatAddressObjectAsParticipants(
[{ address: toParticipants, name: '' }],
'to',
)
: []),
...(cc ? formatAddressObjectAsParticipants([{ address: cc }], 'cc') : []),
...(bcc
? formatAddressObjectAsParticipants([{ address: bcc }], 'bcc')
: []),
]; ];
const textWithoutReplyQuotations = text const textWithoutReplyQuotations = text
@ -57,7 +68,7 @@ export const parseAndFormatGmailMessage = (
subject: subject || '', subject: subject || '',
messageThreadExternalId: threadId, messageThreadExternalId: threadId,
receivedAt: new Date(parseInt(internalDate)), receivedAt: new Date(parseInt(internalDate)),
direction: computeMessageDirection(from[0].address || '', connectedAccount), direction: computeMessageDirection(from || '', connectedAccount),
participants, participants,
text: sanitizeString(textWithoutReplyQuotations), text: sanitizeString(textWithoutReplyQuotations),
attachments, attachments,

View File

@ -1,11 +1,11 @@
import assert from 'assert'; import assert from 'assert';
import addressparser from 'addressparser';
import { gmail_v1 } from 'googleapis'; import { gmail_v1 } from 'googleapis';
import { getAttachmentData } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/get-attachment-data.util'; import { getAttachmentData } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/get-attachment-data.util';
import { getBodyData } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/get-body-data.util'; import { getBodyData } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/get-body-data.util';
import { getPropertyFromHeaders } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/get-property-from-headers.util'; import { getPropertyFromHeaders } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/get-property-from-headers.util';
import { safeParseEmailAddressAddress } from 'src/modules/messaging/message-import-manager/utils/safe-parse.util';
export const parseGmailMessage = (message: gmail_v1.Schema$Message) => { export const parseGmailMessage = (message: gmail_v1.Schema$Message) => {
const subject = getPropertyFromHeaders(message, 'Subject'); const subject = getPropertyFromHeaders(message, 'Subject');
@ -36,11 +36,13 @@ export const parseGmailMessage = (message: gmail_v1.Schema$Message) => {
historyId, historyId,
internalDate, internalDate,
subject, subject,
from: rawFrom ? addressparser(rawFrom) : undefined, from: rawFrom ? safeParseEmailAddressAddress(rawFrom) : undefined,
deliveredTo: rawDeliveredTo ? addressparser(rawDeliveredTo) : undefined, deliveredTo: rawDeliveredTo
to: rawTo ? addressparser(rawTo) : undefined, ? safeParseEmailAddressAddress(rawDeliveredTo)
cc: rawCc ? addressparser(rawCc) : undefined, : undefined,
bcc: rawBcc ? addressparser(rawBcc) : undefined, to: rawTo ? safeParseEmailAddressAddress(rawTo) : undefined,
cc: rawCc ? safeParseEmailAddressAddress(rawCc) : undefined,
bcc: rawBcc ? safeParseEmailAddressAddress(rawBcc) : undefined,
text, text,
attachments, attachments,
}; };

View File

@ -1,5 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { EmailAddress } from 'addressparser';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@ -9,6 +10,7 @@ import { MicrosoftImportDriverException } from 'src/modules/messaging/message-im
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface'; import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
import { MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message'; import { MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util'; import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util';
import { safeParseEmailAddress } from 'src/modules/messaging/message-import-manager/utils/safe-parse.util';
import { MicrosoftFetchByBatchService } from './microsoft-fetch-by-batch.service'; import { MicrosoftFetchByBatchService } from './microsoft-fetch-by-batch.service';
import { MicrosoftHandleErrorService } from './microsoft-handle-error.service'; import { MicrosoftHandleErrorService } from './microsoft-handle-error.service';
@ -78,37 +80,42 @@ export class MicrosoftGetMessagesService {
); );
} }
const participants = [ const safeParseFrom = response?.from?.emailAddress
...formatAddressObjectAsParticipants( ? [safeParseEmailAddress(response.from.emailAddress)]
response?.from?.emailAddress, : [];
'from',
),
...formatAddressObjectAsParticipants(
response?.toRecipients
?.filter(isDefined)
// @ts-expect-error legacy noImplicitAny
.map((recipient) => recipient.emailAddress),
'to',
),
...formatAddressObjectAsParticipants(
response?.ccRecipients
?.filter(isDefined)
// @ts-expect-error legacy noImplicitAny
.map((recipient) => recipient.emailAddress),
'cc',
),
...formatAddressObjectAsParticipants(
response?.bccRecipients
?.filter(isDefined)
// @ts-expect-error legacy noImplicitAny
.map((recipient) => recipient.emailAddress),
'bcc',
),
];
const safeParticipantsFormat = participants.filter((participant) => { const safeParseTo = response?.toRecipients
return participant.handle.includes('@'); ?.filter(isDefined)
}); .map((recipient: { emailAddress: EmailAddress }) =>
safeParseEmailAddress(recipient.emailAddress),
);
const safeParseCc = response?.ccRecipients
?.filter(isDefined)
.map((recipient: { emailAddress: EmailAddress }) =>
safeParseEmailAddress(recipient.emailAddress),
);
const safeParseBcc = response?.bccRecipients
?.filter(isDefined)
.map((recipient: { emailAddress: EmailAddress }) =>
safeParseEmailAddress(recipient.emailAddress),
);
const participants = [
...(safeParseFrom
? formatAddressObjectAsParticipants(safeParseFrom, 'from')
: []),
...(safeParseTo
? formatAddressObjectAsParticipants(safeParseTo, 'to')
: []),
...(safeParseCc
? formatAddressObjectAsParticipants(safeParseCc, 'cc')
: []),
...(safeParseBcc
? formatAddressObjectAsParticipants(safeParseBcc, 'bcc')
: []),
];
return { return {
externalId: response.id, externalId: response.id,
@ -124,7 +131,7 @@ export class MicrosoftGetMessagesService {
connectedAccount, connectedAccount,
) )
: MessageDirection.INCOMING, : MessageDirection.INCOMING,
participants: safeParticipantsFormat, participants,
attachments: [], attachments: [],
}; };
}); });

View File

@ -0,0 +1,4 @@
export type EmailAddress = {
address: string;
name?: string;
};

View File

@ -23,11 +23,42 @@ describe('formatAddressObjectAsParticipants', () => {
]); ]);
}); });
it('should return an empty array if address object is undefined', () => { it('should return an empty array if address object handle has no @', () => {
const addressObject = undefined; const addressObject = {
name: 'John Doe',
address: 'john.doe',
};
const result = formatAddressObjectAsParticipants(addressObject, 'to'); const result = formatAddressObjectAsParticipants([addressObject], 'to');
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
it('should return an empty array if address object handle is empty', () => {
const addressObject = {
name: 'John Doe',
address: '',
};
const result = formatAddressObjectAsParticipants([addressObject], 'to');
expect(result).toEqual([]);
});
it('should return a lowewrcase handle if the handle is not lowercase', () => {
const addressObject = {
name: 'John Doe',
address: 'John.Doe@example.com',
};
const result = formatAddressObjectAsParticipants([addressObject], 'to');
expect(result).toEqual([
{
role: 'to',
handle: 'john.doe@example.com',
displayName: 'John Doe',
},
]);
});
}); });

View File

@ -1,36 +1,33 @@
import addressparser from 'addressparser'; import { isDefined } from 'twenty-shared/utils';
import { Participant } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type'; import { Participant } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type';
import { EmailAddress } from 'src/modules/messaging/message-import-manager/types/email-address';
const formatAddressObjectAsArray = (
addressObject: addressparser.EmailAddress | addressparser.EmailAddress[],
): addressparser.EmailAddress[] => {
return Array.isArray(addressObject) ? addressObject : [addressObject];
};
const removeSpacesAndLowerCase = (email: string): string => { const removeSpacesAndLowerCase = (email: string): string => {
return email.replace(/\s/g, '').toLowerCase(); return email.replace(/\s/g, '').toLowerCase();
}; };
export const formatAddressObjectAsParticipants = ( export const formatAddressObjectAsParticipants = (
addressObject: addressObjects: EmailAddress[],
| addressparser.EmailAddress
| addressparser.EmailAddress[]
| undefined,
role: 'from' | 'to' | 'cc' | 'bcc', role: 'from' | 'to' | 'cc' | 'bcc',
): Participant[] => { ): Participant[] => {
if (!addressObject) return [];
const addressObjects = formatAddressObjectAsArray(addressObject);
const participants = addressObjects.map((addressObject) => { const participants = addressObjects.map((addressObject) => {
const address = addressObject.address; const address = addressObject.address;
if (!isDefined(address)) {
return null;
}
if (!address.includes('@')) {
return null;
}
return { return {
role, role,
handle: address ? removeSpacesAndLowerCase(address) : '', handle: removeSpacesAndLowerCase(address),
displayName: addressObject.name || '', displayName: addressObject.name || '',
}; };
}); });
return participants.flat(); return participants.filter(isDefined) as Participant[];
}; };

View File

@ -0,0 +1,28 @@
import { Logger } from '@nestjs/common';
import addressparser from 'addressparser';
import { EmailAddress } from 'src/modules/messaging/message-import-manager/types/email-address';
export const safeParseEmailAddressAddress = (
address: string,
): string | undefined => {
const logger = new Logger(safeParseEmailAddressAddress.name);
try {
return addressparser(address)[0].address;
} catch (error) {
logger.error(`Error parsing address: ${address}`, error);
return undefined;
}
};
export const safeParseEmailAddress = (
emailAddress: EmailAddress,
): EmailAddress => {
return {
address: safeParseEmailAddressAddress(emailAddress.address) || '',
name: emailAddress.name,
};
};