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:
@@ -13,12 +13,13 @@ import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/i
|
||||
import { isClickHouseConfiguredState } from '@/client-config/states/isClickHouseConfiguredState';
|
||||
import { isCloudflareIntegrationEnabledState } from '@/client-config/states/isCloudflareIntegrationEnabledState';
|
||||
import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
|
||||
import { maintenanceModeState } from '@/client-config/states/maintenanceModeState';
|
||||
import { isEmailGroupEnabledState } from '@/client-config/states/isEmailGroupEnabledState';
|
||||
import { isEmailingDomainsEnabledState } from '@/client-config/states/isEmailingDomainsEnabledState';
|
||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||
import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
|
||||
import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState';
|
||||
import { isImapSmtpCaldavEnabledState } from '@/client-config/states/isImapSmtpCaldavEnabledState';
|
||||
import { maintenanceModeState } from '@/client-config/states/maintenanceModeState';
|
||||
import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicrosoftCalendarEnabledState';
|
||||
import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
@@ -100,13 +101,16 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
|
||||
const setCalendarBookingPageId = useSetAtomState(calendarBookingPageIdState);
|
||||
|
||||
const setIsImapSmtpCaldavEnabled = useSetAtomState(
|
||||
isImapSmtpCaldavEnabledState,
|
||||
);
|
||||
const setIsEmailGroupEnabled = useSetAtomState(isEmailGroupEnabledState);
|
||||
|
||||
const setIsEmailingDomainsEnabled = useSetAtomState(
|
||||
isEmailingDomainsEnabledState,
|
||||
);
|
||||
|
||||
const setIsImapSmtpCaldavEnabled = useSetAtomState(
|
||||
isImapSmtpCaldavEnabledState,
|
||||
);
|
||||
|
||||
const setAllowRequestsToTwentyIcons = useSetAtomState(
|
||||
allowRequestsToTwentyIconsState,
|
||||
);
|
||||
@@ -195,6 +199,7 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
|
||||
setCalendarBookingPageId(clientConfig?.calendarBookingPageId ?? null);
|
||||
setIsImapSmtpCaldavEnabled(clientConfig?.isImapSmtpCaldavEnabled);
|
||||
setIsEmailGroupEnabled(clientConfig?.isEmailGroupEnabled ?? false);
|
||||
setIsEmailingDomainsEnabled(clientConfig?.isEmailingDomainsEnabled);
|
||||
setAllowRequestsToTwentyIcons(clientConfig?.allowRequestsToTwentyIcons);
|
||||
setIsCloudflareIntegrationEnabled(
|
||||
@@ -233,6 +238,7 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
setIsDeveloperDefaultSignInPrefilled,
|
||||
setIsEmailVerificationRequired,
|
||||
setIsImapSmtpCaldavEnabled,
|
||||
setIsEmailGroupEnabled,
|
||||
setIsMultiWorkspaceEnabled,
|
||||
setIsEmailingDomainsEnabled,
|
||||
setIsClickHouseConfigured,
|
||||
|
||||
Reference in New Issue
Block a user