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:
@@ -0,0 +1,4 @@
|
||||
export type ParsedName = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/contact-creation-manager/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
|
||||
|
||||
type TestCase = EachTestingContext<{
|
||||
handle: string;
|
||||
displayName: string;
|
||||
expected: { firstName: string; lastName: string };
|
||||
}>;
|
||||
|
||||
describe('getFirstNameAndLastNameFromHandleAndDisplayName', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title: 'should parse a standard "First Last" display name',
|
||||
context: {
|
||||
handle: 'john.doe@example.com',
|
||||
displayName: 'John Doe',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should swap "Last, First" comma format',
|
||||
context: {
|
||||
handle: 'john.doe@example.com',
|
||||
displayName: 'Doe, John',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should de-duplicate "First.Last Last" synthesized form',
|
||||
context: {
|
||||
handle: 'john.doe@example.com',
|
||||
displayName: 'John.Doe Doe',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should keep all trailing tokens as a multi-word last name',
|
||||
context: {
|
||||
handle: 'mjw@example.com',
|
||||
displayName: 'Mary Jane Watson',
|
||||
expected: { firstName: 'Mary', lastName: 'Jane Watson' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should split a single dotted token into first and last',
|
||||
context: {
|
||||
handle: 'john.doe@example.com',
|
||||
displayName: 'john.doe',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should accept a single-word display name and fall back to handle for last',
|
||||
context: {
|
||||
handle: 'first.someone@example.com',
|
||||
displayName: 'First',
|
||||
expected: { firstName: 'First', lastName: 'Someone' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should fall back to handle when display name is empty',
|
||||
context: {
|
||||
handle: 'john.doe@example.com',
|
||||
displayName: '',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should fall back to handle even when handle has no dot',
|
||||
context: {
|
||||
handle: 'noname@example.com',
|
||||
displayName: '',
|
||||
expected: { firstName: 'Noname', lastName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should strip wrapping quotes before parsing',
|
||||
context: {
|
||||
handle: 'john.doe@example.com',
|
||||
displayName: '"Doe, John"',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should collapse extra whitespace',
|
||||
context: {
|
||||
handle: 'jd@example.com',
|
||||
displayName: ' John Doe ',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should expand "John.Doe Smith" into "John" + "Doe Smith"',
|
||||
context: {
|
||||
handle: 'jd@example.com',
|
||||
displayName: 'John.Doe Smith',
|
||||
expected: { firstName: 'John', lastName: 'Doe Smith' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should preserve multi-dot handle local-parts on fallback',
|
||||
context: {
|
||||
handle: 'jean.luc.picard@example.com',
|
||||
displayName: '',
|
||||
expected: { firstName: 'Jean', lastName: 'Luc picard' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fall back to handle when display name contains the address',
|
||||
context: {
|
||||
handle: 'jane.smith@example.com',
|
||||
displayName: 'Jane.smith@example.com Smith',
|
||||
expected: { firstName: 'Jane', lastName: 'Smith' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fall back to handle for single-token email-as-display-name',
|
||||
context: {
|
||||
handle: 'janesmith@example.com',
|
||||
displayName: 'Janesmith@example.com',
|
||||
expected: { firstName: 'Janesmith', lastName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should strip ":XXX" group-code suffix from comma-format first name',
|
||||
context: {
|
||||
handle: 'jane.smith@example.com',
|
||||
displayName: 'Smith, Jane:GROUP',
|
||||
expected: { firstName: 'Jane', lastName: 'Smith' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should strip ":XXX" suffix from a multi-token display name',
|
||||
context: {
|
||||
handle: 'jane.smith@example.com',
|
||||
displayName: 'Jane:GROUP Smith',
|
||||
expected: { firstName: 'Jane', lastName: 'Smith' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
'$title',
|
||||
({ context: { handle, displayName, expected } }) => {
|
||||
expect(
|
||||
getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName),
|
||||
).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { getParsedNameFromDisplayName } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-display-name.util';
|
||||
|
||||
type TestCase = EachTestingContext<{
|
||||
displayName: string;
|
||||
expected: { firstName: string; lastName: string };
|
||||
}>;
|
||||
|
||||
describe('getParsedNameFromDisplayName', () => {
|
||||
describe('common shapes', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title: 'should split a standard "First Last" display name',
|
||||
context: {
|
||||
displayName: 'John Doe',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should keep all trailing tokens together as a multi-word last name',
|
||||
context: {
|
||||
displayName: 'Mary Jane Watson',
|
||||
expected: { firstName: 'Mary', lastName: 'Jane Watson' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should treat a single-word display name as the first name only',
|
||||
context: {
|
||||
displayName: 'Cher',
|
||||
expected: { firstName: 'Cher', lastName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should collapse surrounding and inner whitespace',
|
||||
context: {
|
||||
displayName: ' John Doe ',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
|
||||
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comma-inverted form "Last, First"', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title: 'should swap the two halves into first and last name',
|
||||
context: {
|
||||
displayName: 'Doe, John',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should strip surrounding RFC 5322 quoted-string quotes before swapping',
|
||||
context: {
|
||||
displayName: '"Doe, John"',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
|
||||
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('email-derived display names', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title: 'should split a single dotted token like an email local part',
|
||||
context: {
|
||||
displayName: 'john.doe',
|
||||
expected: { firstName: 'john', lastName: 'doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should de-duplicate "First.Last Last" forms synthesized by some clients',
|
||||
context: {
|
||||
displayName: 'John.Doe Doe',
|
||||
expected: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should expand "First.Middle Last" forms into a multi-word last name',
|
||||
context: {
|
||||
displayName: 'John.Doe Smith',
|
||||
expected: { firstName: 'John', lastName: 'Doe Smith' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
|
||||
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mail-server ":GROUP" tag suffix', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title:
|
||||
'should strip the tag from the first name in the comma-inverted form',
|
||||
context: {
|
||||
displayName: 'Smith, Jane:GROUP',
|
||||
expected: { firstName: 'Jane', lastName: 'Smith' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should strip the tag from the first token in a space-separated form',
|
||||
context: {
|
||||
displayName: 'Jane:GROUP Smith',
|
||||
expected: { firstName: 'Jane', lastName: 'Smith' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
|
||||
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fallback cases (empty result)', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title:
|
||||
'should return empty names so the caller can fall back to handle parsing when the display name is empty',
|
||||
context: {
|
||||
displayName: '',
|
||||
expected: { firstName: '', lastName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should return empty names when the display name is only whitespace',
|
||||
context: {
|
||||
displayName: ' ',
|
||||
expected: { firstName: '', lastName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should return empty names when forwarders inject an email address into the display-name slot',
|
||||
context: {
|
||||
displayName: 'jane.smith@example.com',
|
||||
expected: { firstName: '', lastName: '' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { displayName, expected } }) => {
|
||||
expect(getParsedNameFromDisplayName(displayName)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { getParsedNameFromEmailLocalPart } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-email-local-part.util';
|
||||
|
||||
type TestCase = EachTestingContext<{
|
||||
localPart: string;
|
||||
expected: { firstName: string; lastName: string };
|
||||
}>;
|
||||
|
||||
describe('getParsedNameFromEmailLocalPart', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title:
|
||||
'should split a dotted local part into first and last name segments',
|
||||
context: {
|
||||
localPart: 'john.doe',
|
||||
expected: { firstName: 'john', lastName: 'doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should keep the trailing dot-segments together as a multi-word last name',
|
||||
context: {
|
||||
localPart: 'jean.luc.picard',
|
||||
expected: { firstName: 'jean', lastName: 'luc picard' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should fall back to the whole local part as first name when no dot is present',
|
||||
context: {
|
||||
localPart: 'noname',
|
||||
expected: { firstName: 'noname', lastName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should ignore consecutive dots so they do not produce empty segments',
|
||||
context: {
|
||||
localPart: 'john..doe',
|
||||
expected: { firstName: 'john', lastName: 'doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should ignore a leading dot rather than producing an empty first name',
|
||||
context: {
|
||||
localPart: '.john.doe',
|
||||
expected: { firstName: 'john', lastName: 'doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should return empty names for an empty local part',
|
||||
context: {
|
||||
localPart: '',
|
||||
expected: { firstName: '', lastName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should strip an RFC 5233 plus-address tag so routing metadata does not end up in the name',
|
||||
context: {
|
||||
localPart: 'john.doe+sales',
|
||||
expected: { firstName: 'john', lastName: 'doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should strip a plus-address tag even when the local part has no dot',
|
||||
context: {
|
||||
localPart: 'noname+ticket-123',
|
||||
expected: { firstName: 'noname', lastName: '' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { localPart, expected } }) => {
|
||||
expect(getParsedNameFromEmailLocalPart(localPart)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { getParsedNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-handle.util';
|
||||
|
||||
type TestCase = EachTestingContext<{
|
||||
handle: string;
|
||||
expected: { firstName: string; lastName: string };
|
||||
}>;
|
||||
|
||||
describe('getParsedNameFromHandle', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title:
|
||||
'should derive first and last name from the local part of a standard email',
|
||||
context: {
|
||||
handle: 'john.doe@example.com',
|
||||
expected: { firstName: 'john', lastName: 'doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should ignore the domain entirely',
|
||||
context: {
|
||||
handle: 'john.doe@deeply.nested.subdomain.example.com',
|
||||
expected: { firstName: 'john', lastName: 'doe' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should return only a first name when the local part has no dot separator',
|
||||
context: {
|
||||
handle: 'noname@example.com',
|
||||
expected: { firstName: 'noname', lastName: '' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should treat an input without an @ as a bare local part rather than rejecting it',
|
||||
context: {
|
||||
handle: 'lonely.handle',
|
||||
expected: { firstName: 'lonely', lastName: 'handle' },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should return empty names for an empty handle',
|
||||
context: {
|
||||
handle: '',
|
||||
expected: { firstName: '', lastName: '' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$title', ({ context: { handle, expected } }) => {
|
||||
expect(getParsedNameFromHandle(handle)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
+12
-11
@@ -1,17 +1,18 @@
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
export function getFirstNameAndLastNameFromHandleAndDisplayName(
|
||||
|
||||
import { type ParsedName } from 'src/modules/contact-creation-manager/types/parsed-name.type';
|
||||
import { getParsedNameFromDisplayName } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-display-name.util';
|
||||
import { getParsedNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-handle.util';
|
||||
|
||||
export const getFirstNameAndLastNameFromHandleAndDisplayName = (
|
||||
handle: string,
|
||||
displayName: string,
|
||||
): { firstName: string; lastName: string } {
|
||||
const firstName = displayName.split(' ')[0];
|
||||
const lastName = displayName.split(' ')[1];
|
||||
|
||||
const contactFullNameFromHandle = handle.split('@')[0];
|
||||
const firstNameFromHandle = contactFullNameFromHandle.split('.')[0];
|
||||
const lastNameFromHandle = contactFullNameFromHandle.split('.')[1];
|
||||
): ParsedName => {
|
||||
const fromDisplayName = getParsedNameFromDisplayName(displayName);
|
||||
const fromHandle = getParsedNameFromHandle(handle);
|
||||
|
||||
return {
|
||||
firstName: capitalize(firstName || firstNameFromHandle || ''),
|
||||
lastName: capitalize(lastName || lastNameFromHandle || ''),
|
||||
firstName: capitalize(fromDisplayName.firstName || fromHandle.firstName),
|
||||
lastName: capitalize(fromDisplayName.lastName || fromHandle.lastName),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ParsedName } from 'src/modules/contact-creation-manager/types/parsed-name.type';
|
||||
import { getParsedNameFromEmailLocalPart } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-email-local-part.util';
|
||||
|
||||
const EMPTY_NAME: ParsedName = { firstName: '', lastName: '' };
|
||||
|
||||
export const getParsedNameFromDisplayName = (
|
||||
displayName: string,
|
||||
): ParsedName => {
|
||||
const cleaned = displayName
|
||||
.trim()
|
||||
.replace(/^['"]+|['"]+$/g, '')
|
||||
.trim();
|
||||
|
||||
if (!isNonEmptyString(cleaned) || cleaned.includes('@')) return EMPTY_NAME;
|
||||
|
||||
const stripTrailingGroupTag = (input: string): string =>
|
||||
input.replace(/:[^:]+$/, '').trim();
|
||||
|
||||
const withGroupTagsStripped = (parsed: ParsedName): ParsedName => ({
|
||||
firstName: stripTrailingGroupTag(parsed.firstName),
|
||||
lastName: stripTrailingGroupTag(parsed.lastName),
|
||||
});
|
||||
|
||||
const commaMatch = cleaned.match(/^([^,]+),\s*([^,]+)$/);
|
||||
|
||||
if (isDefined(commaMatch)) {
|
||||
return withGroupTagsStripped({
|
||||
firstName: commaMatch[2].trim(),
|
||||
lastName: commaMatch[1].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
const [firstToken, ...rest] = cleaned.split(/\s+/);
|
||||
const restAsLastName = rest.join(' ');
|
||||
const { firstName: head, lastName: dotTail } =
|
||||
getParsedNameFromEmailLocalPart(firstToken);
|
||||
|
||||
if (!isNonEmptyString(dotTail)) {
|
||||
return withGroupTagsStripped({
|
||||
firstName: head,
|
||||
lastName: restAsLastName,
|
||||
});
|
||||
}
|
||||
|
||||
const dotTailAlreadyInLastName = restAsLastName
|
||||
.toLowerCase()
|
||||
.startsWith(dotTail.toLowerCase());
|
||||
|
||||
return withGroupTagsStripped({
|
||||
firstName: head,
|
||||
lastName: dotTailAlreadyInLastName
|
||||
? restAsLastName
|
||||
: `${dotTail} ${restAsLastName}`.trim(),
|
||||
});
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type ParsedName } from 'src/modules/contact-creation-manager/types/parsed-name.type';
|
||||
|
||||
export const getParsedNameFromEmailLocalPart = (
|
||||
localPart: string,
|
||||
): ParsedName => {
|
||||
const [withoutPlusAddressTag = ''] = localPart.split('+');
|
||||
const parts = withoutPlusAddressTag.split('.').filter(isNonEmptyString);
|
||||
|
||||
return {
|
||||
firstName: parts[0] ?? '',
|
||||
lastName: parts.slice(1).join(' '),
|
||||
};
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type ParsedName } from 'src/modules/contact-creation-manager/types/parsed-name.type';
|
||||
import { getParsedNameFromEmailLocalPart } from 'src/modules/contact-creation-manager/utils/get-parsed-name-from-email-local-part.util';
|
||||
|
||||
export const getParsedNameFromHandle = (handle: string): ParsedName => {
|
||||
const [localPart = ''] = handle.split('@');
|
||||
|
||||
return getParsedNameFromEmailLocalPart(localPart);
|
||||
};
|
||||
+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