feat(messaging): add workspace toggle to sync internal emails (#20457)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
+6
-1
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
@@ -14,7 +15,11 @@ import { CreatePersonService } from 'src/modules/contact-creation-manager/servic
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity, UserWorkspaceEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
FeatureFlagEntity,
|
||||
UserWorkspaceEntity,
|
||||
WorkspaceEntity,
|
||||
]),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity, FieldMetadataEntity]),
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
|
||||
+7
@@ -5,6 +5,7 @@ import { FieldActorSource } from 'twenty-shared/types';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { CreateCompanyAndPersonService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service';
|
||||
@@ -56,6 +57,12 @@ describe('CreateCompanyAndPersonService', () => {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
+9
@@ -14,6 +14,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
@@ -40,6 +41,8 @@ export class CreateCompanyAndPersonService {
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
async createCompaniesAndPeople(
|
||||
@@ -75,11 +78,17 @@ export class CreateCompanyAndPersonService {
|
||||
|
||||
const workspaceMembers = await workspaceMemberRepository.find();
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
select: ['id', 'isInternalMessagesImportEnabled'],
|
||||
});
|
||||
|
||||
const peopleToCreateFromOtherCompanies =
|
||||
filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contactsToCreate,
|
||||
connectedAccount,
|
||||
workspaceMembers,
|
||||
workspace?.isInternalMessagesImportEnabled ?? false,
|
||||
);
|
||||
|
||||
const { uniqueContacts, uniqueHandles } = getUniqueContactsAndHandles(
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
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 contact = (handle: string): Contact => ({ handle, displayName: handle });
|
||||
|
||||
describe('filterOutContactsThatBelongToSelfOrWorkspaceMembers', () => {
|
||||
it('drops same-domain contacts by default for work domains', () => {
|
||||
const contacts = [contact('alice@acme.com'), contact('bob@other.com')];
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts,
|
||||
account('me@acme.com'),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual([contact('bob@other.com')]);
|
||||
});
|
||||
|
||||
it('keeps same-domain contacts when isInternalMessagesImportEnabled is true', () => {
|
||||
const contacts = [contact('alice@acme.com'), contact('bob@other.com')];
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts,
|
||||
account('me@acme.com'),
|
||||
[],
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual(contacts);
|
||||
});
|
||||
|
||||
it('still drops workspace members and self even when flag is true', () => {
|
||||
const contacts = [
|
||||
contact('alice@acme.com'),
|
||||
contact('me@acme.com'),
|
||||
contact('member@acme.com'),
|
||||
];
|
||||
const workspaceMembers = [
|
||||
{ userEmail: 'member@acme.com' } as WorkspaceMemberWorkspaceEntity,
|
||||
];
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts,
|
||||
account('me@acme.com'),
|
||||
workspaceMembers,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual([contact('alice@acme.com')]);
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -10,6 +10,7 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts: Contact[],
|
||||
connectedAccount: ConnectedAccountEntity,
|
||||
workspaceMembers: WorkspaceMemberWorkspaceEntity[],
|
||||
isInternalMessagesImportEnabled: boolean = false,
|
||||
): Contact[] {
|
||||
if (!isDefined(connectedAccount.handle)) {
|
||||
throw new Error('Connected account handle is missing');
|
||||
@@ -41,7 +42,8 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
return contacts.filter(
|
||||
(contact) =>
|
||||
(isDifferentDomain(contact, selfDomainName) ||
|
||||
!isWorkDomain(selfDomainName)) &&
|
||||
!isWorkDomain(selfDomainName) ||
|
||||
isInternalMessagesImportEnabled) &&
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
!workspaceMembersMap[contact.handle.toLowerCase()] &&
|
||||
!allHandles.includes(contact.handle.toLowerCase()),
|
||||
|
||||
+9
@@ -26,6 +26,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
describe('MessagingMessagesImportService', () => {
|
||||
let service: MessagingMessagesImportService;
|
||||
@@ -183,6 +184,14 @@ describe('MessagingMessagesImportService', () => {
|
||||
findOne: jest.fn().mockResolvedValue({ userId: 'user-id' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ isInternalMessagesImportEnabled: false }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
|
||||
+9
@@ -9,6 +9,7 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
@@ -55,6 +56,8 @@ export class MessagingMessagesImportService {
|
||||
private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
@@ -197,6 +200,11 @@ export class MessagingMessagesImportService {
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
select: ['id', 'isInternalMessagesImportEnabled'],
|
||||
});
|
||||
|
||||
const messagesToSave = filterEmails(
|
||||
messageChannel.handle,
|
||||
[...connectedAccountWithFreshTokens.handleAliases],
|
||||
@@ -205,6 +213,7 @@ export class MessagingMessagesImportService {
|
||||
.map((blocklistItem) => blocklistItem.handle)
|
||||
.filter(isDefined),
|
||||
messageChannel.excludeGroupEmails,
|
||||
workspace?.isInternalMessagesImportEnabled ?? false,
|
||||
);
|
||||
|
||||
if (messagesToSave.length > 0) {
|
||||
|
||||
+21
@@ -37,6 +37,27 @@ describe('filterEmails', () => {
|
||||
expect(filteredMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('Should keep same-domain emails when isInternalMessagesImportEnabled is true', () => {
|
||||
// Workspace opted into syncing internal emails (e.g. university or
|
||||
// shared-domain institution). Same-domain participants must not be
|
||||
// dropped — the toggle bypasses filterOutInternals.
|
||||
const primaryHandle = 'guillim@acme.com';
|
||||
const messages = messagingGetMessagesServiceGetMessages.filter(
|
||||
(message) => message.externalId === 'AA-work-emails-internal',
|
||||
);
|
||||
|
||||
const filteredMessages = filterEmails(
|
||||
primaryHandle,
|
||||
[],
|
||||
messages,
|
||||
[],
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(filteredMessages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('Should filter messages with participant from the blocklist', () => {
|
||||
const primaryHandle = 'guillim@acme.com';
|
||||
const messages = messagingGetMessagesServiceGetMessages.filter(
|
||||
|
||||
+5
-1
@@ -15,6 +15,7 @@ export const filterEmails = (
|
||||
messages: MessageWithParticipants[],
|
||||
blocklist: string[],
|
||||
excludeGroupEmails: boolean = true,
|
||||
isInternalMessagesImportEnabled: boolean = false,
|
||||
) => {
|
||||
const messagesWithoutIcsAttachments = filterOutIcsAttachments(messages);
|
||||
|
||||
@@ -24,7 +25,10 @@ export const filterEmails = (
|
||||
blocklist,
|
||||
);
|
||||
|
||||
const messagesWithoutInternals = isWorkEmail(primaryHandle)
|
||||
const shouldFilterOutInternals =
|
||||
isWorkEmail(primaryHandle) && !isInternalMessagesImportEnabled;
|
||||
|
||||
const messagesWithoutInternals = shouldFilterOutInternals
|
||||
? filterOutInternals(primaryHandle, messagesWithoutBlocklisted)
|
||||
: messagesWithoutBlocklisted;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user