feat: add email forwarding message channel (#19535)
## Summary - Add email forwarding as a new message channel type, allowing users to forward emails from addresses like `support@mycompany.com` into Twenty - Inbound emails arrive via S3 (SES → S3 bucket), are polled by a cron job, parsed, routed to the correct workspace/channel, and persisted as messages - Dedicated settings page at `/settings/accounts/new-email-forwarding` where users provide their source email handle and receive a unique forwarding address - Forwarding channels bypass the IMAP/mailbox sync state machine — they skip cron-driven sync, relaunch, and message-list-fetch lifecycle stages - Forwarding address section shown at the top of the Emails settings page so users can find/copy their addresses after initial setup - Tab names for forwarding channels display the user-provided handle (e.g. `support@mycompany.com`) instead of the internal routing address - Shared utilities extracted from IMAP driver: `extractThreadId`, `extractParticipants`, `extractAddresses` to avoid code duplication - Uses the existing S3 bucket (STORAGE_S3_*) with `inbound-email/` prefix — no separate bucket needed - Feature gated behind `isEmailForwardingEnabled` client config (requires `INBOUND_EMAIL_DOMAIN` + S3 storage) ## New backend modules - `InboundEmailS3ClientProvider` — lazy-initialized S3 client using existing storage config - `InboundEmailStorageService` — S3 operations (get, move to processed/unmatched/failed) - `InboundEmailParserService` — RFC 822 parsing via `postal-mime`, builds `MessageWithParticipants` - `InboundEmailImportService` — orchestrates download → parse → route → persist → archive - `MessagingInboundEmailPollCronJob` — polls S3 `incoming/` prefix, enqueues import jobs - `CreateEmailForwardingChannelInput` DTO — accepts user-provided `handle` ## New frontend components - `SettingsAccountsNewEmailForwardingChannel` — dedicated page with handle input form + forwarding address result - `SettingsAccountsEmailForwardingSection` — forwarding address list on the Emails settings page - `useConnectedAccountHandleMap` — shared hook for account ID → handle lookup - `useCreateEmailForwardingChannel` — mutation hook accepting handle parameter ## Test plan - [x] 17 unit tests for inbound email import service (all outcomes: imported, unmatched, loop_dropped, unconfigured, parse_failed, persist_failed) - [x] 16 tests for `computeSyncStatus` including EMAIL_FORWARDING cases - [x] 11 tests for `extractEnvelopeRecipient` utility - [x] TypeScript typechecks pass for both twenty-server and twenty-front - [x] Lint passes for both packages - [ ] Manual: create forwarding channel, verify forwarding address generated - [ ] Manual: send email to forwarding address, verify it appears in Twenty https://claude.ai/code/session_01KpyF6p4cUEnuaT4h8DP5Pm --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: neo773 <neo773@protonmail.com>
This commit is contained in:
+1
@@ -94,6 +94,7 @@ describe('ClientConfigController', () => {
|
||||
isGoogleCalendarEnabled: false,
|
||||
isConfigVariablesInDbEnabled: false,
|
||||
isImapSmtpCaldavEnabled: false,
|
||||
isEmailGroupEnabled: false,
|
||||
calendarBookingPageId: undefined,
|
||||
isTwoFactorAuthenticationEnabled: false,
|
||||
allowRequestsToTwentyIcons: true,
|
||||
|
||||
@@ -309,6 +309,9 @@ export class ClientConfig {
|
||||
@Field(() => Boolean)
|
||||
isImapSmtpCaldavEnabled: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
isEmailGroupEnabled: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
allowRequestsToTwentyIcons: boolean;
|
||||
|
||||
|
||||
+1
@@ -171,6 +171,7 @@ describe('ClientConfigService', () => {
|
||||
isGoogleCalendarEnabled: true,
|
||||
isConfigVariablesInDbEnabled: false,
|
||||
isImapSmtpCaldavEnabled: false,
|
||||
isEmailGroupEnabled: false,
|
||||
allowRequestsToTwentyIcons: false,
|
||||
calendarBookingPageId: 'team/twenty/talk-to-us',
|
||||
isCloudflareIntegrationEnabled: false,
|
||||
|
||||
+6
@@ -4,6 +4,8 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type AiSdkPackage } from 'twenty-shared/ai';
|
||||
|
||||
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces/file-storage.interface';
|
||||
|
||||
import {
|
||||
AI_SDK_ANTHROPIC,
|
||||
AI_SDK_BEDROCK,
|
||||
@@ -247,6 +249,10 @@ export class ClientConfigService {
|
||||
isImapSmtpCaldavEnabled: this.twentyConfigService.get(
|
||||
'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||
),
|
||||
isEmailGroupEnabled:
|
||||
this.twentyConfigService.get('STORAGE_TYPE') ===
|
||||
StorageDriverType.S_3 &&
|
||||
isNonEmptyString(this.twentyConfigService.get('INBOUND_EMAIL_DOMAIN')),
|
||||
allowRequestsToTwentyIcons: this.twentyConfigService.get(
|
||||
'ALLOW_REQUESTS_TO_TWENTY_ICONS',
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user