Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a5daba10 | ||
|
|
285b66b63c | ||
|
|
6c6760495b |
+85
@@ -0,0 +1,85 @@
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
|
||||
import { filterOutContactsThatBelongToSelfOrWorkspaceMembers } from 'src/modules/contact-creation-manager/utils/filter-out-contacts-that-belong-to-self-or-workspace-members.util';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
const account = (
|
||||
handle: string,
|
||||
handleAliases: string[] = [],
|
||||
): ConnectedAccountEntity =>
|
||||
({ handle, handleAliases }) as ConnectedAccountEntity;
|
||||
|
||||
const member = (userEmail: string): WorkspaceMemberWorkspaceEntity =>
|
||||
({ userEmail }) as unknown as WorkspaceMemberWorkspaceEntity;
|
||||
|
||||
const contact = (handle: string): Contact => ({ handle, displayName: handle });
|
||||
|
||||
describe('filterOutContactsThatBelongToSelfOrWorkspaceMembers', () => {
|
||||
it('drops contacts sharing a company domain with the connected account', () => {
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('colleague@acme.com'), contact('lead@prospect.com')],
|
||||
account('me@acme.com'),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result.map((c) => c.handle)).toEqual(['lead@prospect.com']);
|
||||
});
|
||||
|
||||
it('keeps same-domain contacts when the connected account is on a consumer email provider', () => {
|
||||
// gmail.com is shared by millions of unrelated people, so domain
|
||||
// equality cannot mean "teammate".
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('friend@gmail.com')],
|
||||
account('me@gmail.com'),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps same-domain contacts when the connected account is on a university domain', () => {
|
||||
// Universities host thousands of unrelated members; treat them like
|
||||
// consumer providers and let collaborators through.
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('professor@mit.edu'), contact('lab-member@mit.edu')],
|
||||
account('researcher@mit.edu'),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('drops workspace members regardless of domain', () => {
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('teammate@partner.com'), contact('outsider@partner.com')],
|
||||
account('me@acme.com'),
|
||||
[member('teammate@partner.com')],
|
||||
);
|
||||
|
||||
expect(result.map((c) => c.handle)).toEqual(['outsider@partner.com']);
|
||||
});
|
||||
|
||||
it('drops the connected account handle and any of its aliases', () => {
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[
|
||||
contact('me@acme.com'),
|
||||
contact('me+work@acme.com'),
|
||||
contact('real-contact@partner.com'),
|
||||
],
|
||||
account('me@acme.com', ['me+work@acme.com']),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result.map((c) => c.handle)).toEqual(['real-contact@partner.com']);
|
||||
});
|
||||
|
||||
it('matches handles case-insensitively', () => {
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('Teammate@Partner.com')],
|
||||
account('me@acme.com'),
|
||||
[member('teammate@partner.com')],
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -4,6 +4,7 @@ import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connect
|
||||
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
|
||||
import { getDomainNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-domain-name-from-handle.util';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { isInstitutionalDomain } from 'src/utils/is-institutional-email';
|
||||
import { isWorkDomain } from 'src/utils/is-work-email';
|
||||
|
||||
export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
@@ -41,7 +42,8 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
return contacts.filter(
|
||||
(contact) =>
|
||||
(isDifferentDomain(contact, selfDomainName) ||
|
||||
!isWorkDomain(selfDomainName)) &&
|
||||
!isWorkDomain(selfDomainName) ||
|
||||
isInstitutionalDomain(selfDomainName)) &&
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
!workspaceMembersMap[contact.handle.toLowerCase()] &&
|
||||
!allHandles.includes(contact.handle.toLowerCase()),
|
||||
|
||||
+37
@@ -37,6 +37,43 @@ describe('filterEmails', () => {
|
||||
expect(filteredMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('Should keep same-domain emails when primary handle is on a university domain', () => {
|
||||
// Universities host thousands of unrelated members across departments
|
||||
// and labs, so domain equality is not a reliable proxy for "internal
|
||||
// teammate". Treat them like consumer providers and skip the same-domain
|
||||
// filter — collaborators sharing the institution domain should be
|
||||
// imported, not silently dropped.
|
||||
const primaryHandle = 'researcher@mit.edu';
|
||||
const messages: MessageWithParticipants[] = [
|
||||
{
|
||||
externalId: 'institution-internal',
|
||||
subject: 'Research collaboration',
|
||||
receivedAt: new Date('2025-01-09T09:54:37.000Z'),
|
||||
text: 'Following up on the paper.',
|
||||
headerMessageId: '<msg@mit.edu>',
|
||||
messageThreadExternalId: 'thread-1',
|
||||
direction: MessageDirection.OUTGOING,
|
||||
participants: [
|
||||
{
|
||||
role: MessageParticipantRole.FROM,
|
||||
handle: 'researcher@mit.edu',
|
||||
displayName: 'Researcher',
|
||||
},
|
||||
{
|
||||
role: MessageParticipantRole.TO,
|
||||
handle: 'professor@mit.edu',
|
||||
displayName: 'Professor',
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = filterEmails(primaryHandle, [], messages, []);
|
||||
|
||||
expect(result).toEqual(messages);
|
||||
});
|
||||
|
||||
it('Should filter messages with participant from the blocklist', () => {
|
||||
const primaryHandle = 'guillim@acme.com';
|
||||
const messages = messagingGetMessagesServiceGetMessages.filter(
|
||||
|
||||
+7
-3
@@ -7,6 +7,7 @@ import { filterOutIcsAttachments } from 'src/modules/messaging/message-import-ma
|
||||
import { filterOutInternals } from 'src/modules/messaging/message-import-manager/utils/filter-out-internals.util';
|
||||
import { isGroupEmail } from 'src/modules/messaging/message-import-manager/utils/is-group-email';
|
||||
import { isMessageSenderMatchingHandles } from 'src/modules/messaging/message-import-manager/utils/is-message-sender-matching-handles.util';
|
||||
import { isInstitutionalEmail } from 'src/utils/is-institutional-email';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
|
||||
export const filterEmails = (
|
||||
@@ -24,9 +25,12 @@ export const filterEmails = (
|
||||
blocklist,
|
||||
);
|
||||
|
||||
const messagesWithoutInternals = isWorkEmail(primaryHandle)
|
||||
? filterOutInternals(primaryHandle, messagesWithoutBlocklisted)
|
||||
: messagesWithoutBlocklisted;
|
||||
const isSharedDomain =
|
||||
!isWorkEmail(primaryHandle) || isInstitutionalEmail(primaryHandle);
|
||||
|
||||
const messagesWithoutInternals = isSharedDomain
|
||||
? messagesWithoutBlocklisted
|
||||
: filterOutInternals(primaryHandle, messagesWithoutBlocklisted);
|
||||
|
||||
if (!excludeGroupEmails) {
|
||||
return messagesWithoutInternals;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
isInstitutionalDomain,
|
||||
isInstitutionalEmail,
|
||||
} from 'src/utils/is-institutional-email';
|
||||
|
||||
describe('isInstitutionalDomain', () => {
|
||||
it('returns true for universities in the Hipolabs dataset', () => {
|
||||
expect(isInstitutionalDomain('mit.edu')).toBe(true);
|
||||
expect(isInstitutionalDomain('cam.ac.uk')).toBe(true);
|
||||
expect(isInstitutionalDomain('ethz.ch')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for one-level subdomains under a known university', () => {
|
||||
expect(isInstitutionalDomain('stud.tu-darmstadt.de')).toBe(true);
|
||||
expect(isInstitutionalDomain('cs.stanford.edu')).toBe(true);
|
||||
expect(isInstitutionalDomain('alumni.mit.edu')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for company-owned domains', () => {
|
||||
expect(isInstitutionalDomain('acme.com')).toBe(false);
|
||||
expect(isInstitutionalDomain('example.io')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not match bare TLDs via parent walk', () => {
|
||||
expect(isInstitutionalDomain('de')).toBe(false);
|
||||
expect(isInstitutionalDomain('edu')).toBe(false);
|
||||
expect(isInstitutionalDomain('random.de')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not treat consumer providers as institutional', () => {
|
||||
expect(isInstitutionalDomain('gmail.com')).toBe(false);
|
||||
expect(isInstitutionalDomain('outlook.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInstitutionalEmail', () => {
|
||||
it('extracts the domain and applies the institutional rules', () => {
|
||||
expect(isInstitutionalEmail('researcher@mit.edu')).toBe(true);
|
||||
expect(isInstitutionalEmail('student@stud.tu-darmstadt.de')).toBe(true);
|
||||
expect(isInstitutionalEmail('user@acme.com')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false rather than throwing on malformed input', () => {
|
||||
expect(isInstitutionalEmail('')).toBe(false);
|
||||
expect(isInstitutionalEmail('not-an-email')).toBe(false);
|
||||
expect(isInstitutionalEmail('missing-domain@')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||
import { universityDomainsSet } from 'src/utils/university-domains';
|
||||
|
||||
export const isInstitutionalDomain = (domain: string) => {
|
||||
if (universityDomainsSet.has(domain)) return true;
|
||||
|
||||
const dot = domain.indexOf('.');
|
||||
const parent = dot === -1 ? '' : domain.slice(dot + 1);
|
||||
|
||||
return parent.includes('.') && universityDomainsSet.has(parent);
|
||||
};
|
||||
|
||||
export const isInstitutionalEmail = (email: string) => {
|
||||
try {
|
||||
return isInstitutionalDomain(getDomainNameByEmail(email));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user