4da8878697
## 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>
40 lines
1.4 KiB
TypeScript
40 lines
1.4 KiB
TypeScript
import { GMAIL_COMPOSE_SCOPE } from '@/accounts/constants/GmailComposeScope';
|
|
import { MICROSOFT_SEND_SCOPE } from '@/accounts/constants/MicrosoftSendScope';
|
|
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
|
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
|
|
|
export const getMissingDraftEmailScopes = (
|
|
connectedAccount: Pick<ConnectedAccount, 'provider' | 'scopes'>,
|
|
): string[] => {
|
|
const scopes = connectedAccount.scopes;
|
|
|
|
switch (connectedAccount.provider) {
|
|
case ConnectedAccountProvider.GOOGLE: {
|
|
const hasScope =
|
|
isDefined(scopes) &&
|
|
scopes.some((scope) => scope === GMAIL_COMPOSE_SCOPE);
|
|
|
|
return hasScope ? [] : [GMAIL_COMPOSE_SCOPE];
|
|
}
|
|
case ConnectedAccountProvider.MICROSOFT: {
|
|
const hasScope =
|
|
isDefined(scopes) &&
|
|
scopes.some((scope) => scope === MICROSOFT_SEND_SCOPE);
|
|
|
|
return hasScope ? [] : [MICROSOFT_SEND_SCOPE];
|
|
}
|
|
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
|
|
case ConnectedAccountProvider.OIDC:
|
|
case ConnectedAccountProvider.SAML:
|
|
case ConnectedAccountProvider.EMAIL_GROUP:
|
|
case ConnectedAccountProvider.APP:
|
|
return [];
|
|
default:
|
|
assertUnreachable(
|
|
connectedAccount.provider,
|
|
'Provider not yet supported for draft email actions',
|
|
);
|
|
}
|
|
};
|