fix(contact-creation): handle common email display-name shapes when auto-creating People (#20639)
## Summary
When messages are imported, Twenty auto-creates a Person record for any
recipient that doesn't exist yet. The display-name parser used at that
point is `displayName.split(' ')[0] / [1]`, which silently mangles
several common header shapes:
| Header | Old result |
|-------------------------------------------------|-----------------------------------------|
| `"Doe, John" <...>` | `firstName="Doe,"`, `lastName="John"` |
| `"John.Doe Doe" <...>` | `firstName="John.Doe"`, `lastName="Doe"`|
| `"Mary Jane Watson" <...>` | `lastName="Jane"` ("Watson" dropped) |
| `"john.doe@x.com" <john.doe@x.com>` (forwarder) | full address in
`firstName` |
| `"Doe, John:GROUP" <...>` (group-tag servers) |
`firstName="John:GROUP"` |
This PR rewrites `getFirstNameAndLastNameFromHandleAndDisplayName` to
handle each pattern. Behaviour in order:
1. Trim + strip wrapping quotes
2. Swap `"Last, First"` comma form
3. Fall back to handle parsing when display name contains `@` (real
names don't)
4. Split single dotted tokens (`"john.doe"` → `"John"`, `"Doe"`)
5. Preserve multi-word last names (`tokens.slice(1).join(' ')`)
6. De-synthesize dot-glued first names (`"John.Doe Doe"` → `"John"`,
`"Doe"`)
7. Strip `:XXX` trailing tag suffix from each parsed field
## Test plan
- [x] 16 new unit test cases covering each shape
(`__tests__/get-first-name-and-last-name-from-handle-and-display-name.util.spec.ts`)
- [x] Lint + typecheck clean
- [ ] No regression in the messaging import flow
---------
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
This commit is contained in:
+19
@@ -67,6 +67,25 @@ describe('parseAndFormatGmailMessage', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should preserve the display name on the FROM participant', () => {
|
||||
const result = parseAndFormatGmailMessage(
|
||||
buildMessage([
|
||||
{ name: 'From', value: '"Doe, John" <john.doe@example.com>' },
|
||||
{ name: 'To', value: 'me@example.com' },
|
||||
{ name: 'Message-ID', value: '<abc@example.com>' },
|
||||
]),
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
const fromParticipant = result?.participants.find((p) => p.role === 'FROM');
|
||||
|
||||
expect(fromParticipant).toEqual({
|
||||
role: 'FROM',
|
||||
handle: 'john.doe@example.com',
|
||||
displayName: 'Doe, John',
|
||||
});
|
||||
});
|
||||
|
||||
it('should mark messages from the connected account as OUTGOING', () => {
|
||||
const result = parseAndFormatGmailMessage(
|
||||
buildMessage([
|
||||
|
||||
+2
-5
@@ -42,10 +42,7 @@ export const parseAndFormatGmailMessage = (
|
||||
: [];
|
||||
|
||||
const participants = [
|
||||
...formatAddressObjectAsParticipants(
|
||||
[{ address: from }],
|
||||
MessageParticipantRole.FROM,
|
||||
),
|
||||
...formatAddressObjectAsParticipants([from], MessageParticipantRole.FROM),
|
||||
...formatAddressObjectAsParticipants(
|
||||
toParticipants,
|
||||
MessageParticipantRole.TO,
|
||||
@@ -72,7 +69,7 @@ export const parseAndFormatGmailMessage = (
|
||||
subject: subject || '',
|
||||
messageThreadExternalId: threadId,
|
||||
receivedAt: new Date(parseInt(internalDate)),
|
||||
direction: computeMessageDirection(from || '', connectedAccount),
|
||||
direction: computeMessageDirection(from.address || '', connectedAccount),
|
||||
participants,
|
||||
text: sanitizeString(textWithoutReplyQuotations),
|
||||
attachments,
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ export const parseGmailMessage = (message: gmail_v1.Schema$Message) => {
|
||||
historyId,
|
||||
internalDate,
|
||||
subject,
|
||||
from: rawFrom ? safeParseEmailAddressAddress(rawFrom) : undefined,
|
||||
from: rawFrom ? safeParseEmailAddresses(rawFrom)[0] : undefined,
|
||||
deliveredTo: rawDeliveredTo
|
||||
? safeParseEmailAddressAddress(rawDeliveredTo)
|
||||
: undefined,
|
||||
|
||||
Reference in New Issue
Block a user