Commit Graph

445 Commits

Author SHA1 Message Date
Paul Rastoin 164a5b1e8d Refactor email composer (#21177)
# Introduction
Gate what connected account can be ingested in case of ai mcp user
workspace agnostic funnel to only the workspace shared connected account

Added a quick win intregration tests on seeded connected accounts ( that
wasn't covered but already protected fix impacts only the mcp )

Refactored the API slightly too

## Notice
This mean there's a breaking change in the product behavior
Whereas before a non user workspace related mcp interaction would might
have fallback on any private user connected account it will now only
search for workspace visible listed ones
2026-06-03 15:15:51 +00:00
Anish Paudel b422550fcc refactor(server): merge duplicate TypeOrmModule.forFeature calls in MessagingMessageCleanerModule (#21150)
Combined two separate `TypeOrmModule.forFeature()` calls into one. Both
registered entities on the default data source, so no behavioral change.
Repositories for WorkspaceEntity and MessageChannelEntity remain
injectable as before.
Cleaner imports, one less redundant call.
2026-06-02 14:10:36 +00:00
Félix Malfait f4ead89956 refactor(twenty-orm): migrate 23 grandfathered entities to WorkspaceScopedRepository (#20987)
## Summary

Follow-up to #20953. Migrates 23 of the 30 entities that were left in
`WORKSPACE_SCOPED_EXEMPTIONS` last time, so the lint rule's
workspaceId-enforcement default now covers most of the core/metadata
schema.

### Migrated (23 entities, 88 files, 22 commits)

| Family | Entities |
|---|---|
| Trivial caches | `NavigationMenuItem`, `Skill`, `DataSource`,
`Webhook`, `CommandMenuItem`, `IndexMetadata` |
| Views | `View`, `ViewField`, `ViewFieldGroup`, `ViewFilter`,
`ViewFilterGroup`, `ViewGroup`, `ViewSort` |
| Layouts | `PageLayout`, `PageLayoutTab`, `PageLayoutWidget` |
| Roles & permissions | `Role`, `RoleTarget`, `PermissionFlag`,
`ObjectPermission`, `FieldPermission`, `RowLevelPermissionPredicate`,
`RowLevelPermissionPredicateGroup` |

For each entity: swap `@InjectRepository(X)` →
`@InjectWorkspaceScopedRepository(X)` (and the field type →
`WorkspaceScopedRepository<X>`); rewrite every call site to pass
`workspaceId` as the first arg (stripped from `where`/criteria — the
wrapper throws if you include it now); register
`provideWorkspaceScopedRepository(X)` in every owning NestJS module;
update affected spec providers to
`getWorkspaceScopedRepositoryToken(X)`.

### Rule update

- `ApplicationRegistrationVariableEntity` was misclassified — moved to
`STRUCTURAL_EXEMPTIONS` (no `workspaceId` column; it's keyed on
`applicationRegistrationId` at the instance level).
- 22 of the 23 migrated entities removed from
`WORKSPACE_SCOPED_EXEMPTIONS` entirely (zero remaining raw
`@InjectRepository` sites).
- `RoleTargetEntity` also removed; one call site in
`user-workspace.service.ts` keeps a raw injection with an
`eslint-disable` + reason because `softRemove(...)` is not on the
wrapper API yet (the migration would require threading `workspaceId`
through `deleteUserWorkspace`'s three callers).

### Still exempted (7 entities, follow-up PRs)

| Entity | Why deferred |
|---|---|
| `ApplicationEntity` | ~50 sites with several cross-workspace lookups
by id (auth, OAuth, file-storage, cleanup) |
| `CalendarChannelEntity` / `MessageChannelEntity` | Use
`.increment(...)` (not on wrapper) and
`repository.manager.transaction(...)` — wrapper needs to grow
`.increment` + the transaction sites need `withManager` or dual-inject |
| `FieldMetadataEntity` / `ObjectMetadataEntity` | The metadata services
`extends TypeOrmQueryService<X>` and `super(rawRepo)` — requires
dual-inject or reworking the inheritance |
| `KeyValuePairEntity` | Allows `workspaceId: IsNull()` for
instance-level config; wrapper rejects null |
| `UpgradeMigrationEntity` | Same — instance-level + cross-workspace
ledger |

## Test plan

- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx lint twenty-server` — clean (0/0)
- [x] All 10 affected unit specs pass (115 tests) — api-key, agent-role,
permissions, workspace-roles-permissions-cache, view-filter-group,
workflow-version-step-operations, two-factor-authentication (service +
resolver), user-workspace, file
- [ ] Server integration tests in CI
2026-05-28 20:46:21 +02:00
Paul Rastoin ebfaca5b3d EncryptedString PlaintextString branded string types (#21001)
## Summary

closes https://github.com/twentyhq/core-team-issues/issues/2464

Introduces compile-time branded types to distinguish encrypted
ciphertext from plaintext strings, preventing mix-ups like the one fixed
in #20819 — but at the type level rather in addition to the one existing
at runtime.

### Branded string primitives

- Created `EncryptedString` and `PlaintextString` as hard nominal brands
using `z.string().brand(...)`, making them non-assignable to each other
or to raw `string`
- Created `isEncryptedString` type predicate to narrow `string` to
`EncryptedString` based on the `enc:v2:` envelope prefix
- Retyped `SecretEncryptionService`: `encryptVersioned` accepts
`PlaintextString`, `decryptVersioned` returns `PlaintextString`

### Entity typing

- Typed encrypted columns across entities:
`SigningKeyEntity.privateKey`,
`TwoFactorAuthenticationMethodEntity.secret`,
`ApplicationRegistrationVariableEntity.encryptedValue`,
`ApplicationVariableEntity.value`
- Parameterized JSONB types for connected account connection parameters
(`ImapSmtpCaldavParams<Pwd>`) with reusable aliases
`EncryptedImapSmtpCaldavParams` / `DecryptedImapSmtpCaldavParams`
- Typed DTOs (`CreateApplicationRegistrationVariableInput`,
`UpdateApplicationRegistrationVariablePayload`,
`UpdateApplicationVariableEntityInput`) with `PlaintextString`

### ApplicationVariable always-encrypt uniformization

- Retyped `ApplicationVariableEntity.value` to `EncryptedString | ''` —
all values are now encrypted regardless of `isSecret`
- Updated `ApplicationVariableEntityService` to always encrypt on write
and always decrypt on read
- Simplified `UpdateApplicationVariableActionHandlerService` by removing
conditional encrypt/decrypt-on-isSecret-toggle logic
- Added slow instance command (`2.9.0`) to backfill-encrypt existing
`isSecret=false` plaintext rows and tighten the `CHECK` constraint

### ConfigStorageService refactor

- Split `convertAndSecureValue` (which used `any`) into two well-typed
methods: `convertAndDecrypt` and `convertAndEncrypt`
- Introduced `isSensitiveStringValue` type predicate to narrow values
before encryption/decryption

### What's next
- Typeorm entity derivation to strictly type sitemap configuration as
code + handler logic for encryption rotation
- https://github.com/twentyhq/core-team-issues/issues/2465
2026-05-28 17:41:16 +02:00
neo773 c5606212f2 Ses outbound followup (#20610)
This pull request unifies outbound with inbound under the new feature
and the new email groups feature.

These are workspace level shared inboxes that are shared between all
workspace members.

outbound sending with SES works, we only listen for tenant status
events, rest is managed by AWS

PR refactors old code and webhook to be split for outbound and inbound
for proper separation


| Area | Change |
|---|---|
| AWS SES driver | Split into `AwsSesRegisterDomainService` (tenant +
identity + DKIM + MAIL FROM + configuration-set + EventBridge dest +
contact list) and `AwsSesSendEmailService` (SendEmail). |
| Reputation webhook | New `/webhooks/messaging/ses/outbound` route. SES
→ EventBridge (`Sending Status Enabled/Disabled` on default bus) → SNS →
router → `SesOutboundSendingStateHandlerService` updates
`emailing_domain.tenantStatus`. |
| Inbound webhook | Refactored into `SesInboundWebhookRouterService` +
`SesInboundMailHandlerService`. Shared `SnsSignatureVerifierService` +
`SnsSubscriptionConfirmerService` across both routes. |
| Global uniqueness | New migration + instance command:
`emailing_domain.domain` is now globally unique (one tenant per domain
across workspaces). |
| Tenant status | New `emailing_domain.tenantStatus` column (`ACTIVE` /
`PAUSED`) + `EmailingDomainTenantStatusService`. |
| Send-email mutation | New `sendEmailViaDomain` GraphQL mutation +
DTOs. |
| Cleanup | `EmailingDomainWorkspaceCleanupJob` wired into
`WorkspaceService.deleteWorkspace` — tears down SES tenant association +
identity on workspace delete. |
| Settings UI | Rewritten around reusable `SettingsTableListSection`.
"Email Group" → "Email Handle" rename. New cells for
status/source/forwarding. Outbound domains surfaced on workspace
settings page. |

### Env vars (new)

All in `config-variables.ts`, group `AWS_SES_SETTINGS`, all optional:

- `AWS_SES_REGION` — `@IsAWSRegion`, consumed by `AwsSesClientProvider`
+ driver factory
- `AWS_SES_ACCOUNT_ID` — used for ARN construction in driver factory
- `SES_SNS_TOPIC_ARN_ALLOWLIST` — **shared** by inbound + outbound
webhook routers, comma-separated list of accepted SNS topic ARNs
(verified via `sns-payload-validator`)

### Migrations

- `1778862608620-add-emailing-domain-tenant-status` (fast) — adds
`tenantStatus` column.
- `1778865501791-unique-emailing-domain-globally` (slow, idempotent) —
enforces global uniqueness on `domain`.
- Instance commands bumped to `2.5`.

### Infra dependency

Two coupled twenty-infra PRs:

- `ses-inbound-email` — receipt-rule + inbound SNS topic + S3 bucket
policy + KMS grant + `email_group_*` outputs.
- `ses-outbound-tf` — EventBridge rule + outbound SNS topic + SES IAM
policy + outbound `webhook_url` subscription. **Based on
`ses-inbound-email`.**

Merge order: inbound first, then outbound. Outbound PR's chart edit owns
the comma-joined `SES_SNS_TOPIC_ARN_ALLOWLIST` value (both ARNs).


Features lives under `/settings/general`

<img width="1496" height="845" alt="SCR-20260519-ofhi-2"
src="https://github.com/user-attachments/assets/a025485a-09f7-4131-91cd-0067690ff18d"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-27 19:38:44 +02:00
Félix Malfait 4797d2f270 feat(twenty-orm): introduce WorkspaceScopedRepository for core/metadata workspace-scoped entities (#20953)
## Summary

Adds a third tenancy enforcement layer for entities that live in shared
schemas (`core`, `metadata`) and carry a `workspaceId` column —
previously the only safeguard at this layer was developer discipline
(remembering to put `workspaceId` in every WHERE clause).

### The three layers, after this PR

| Layer | Scope | How it's enforced |
|---|---|---|
| 1. Workspace data | per-workspace schema (companies, people, custom
objects) | `twentyORMManager.getRepository(workspace, E)` — physical
isolation (own data source) |
| 2. Metadata | shared `metadata` schema (objectMetadata, fieldMetadata,
views, roles…) | Flat-entity-maps cache — workspace-scoped in-memory
map, lookups by id within it |
| 3. Core (new) | shared `core` schema (agent threads/turns/messages,
app tokens, etc.) | `WorkspaceScopedRepository<T>` — `workspaceId` is a
required positional argument on every read/write |

## What's in the PR

### The wrapper
(`packages/twenty-server/src/engine/twenty-orm/workspace-scoped-repository/`)
- `WorkspaceScopedRepository<T extends WorkspaceScopedEntity>` — wraps a
TypeORM `Repository<T>`, requires `workspaceId` on every
`find`/`findOne`/`findOneOrFail`/`update`/`delete`/`softDelete`/`insert`/`save`/`count`
call, merging it into the WHERE or stamping it on the entity.
`createQueryBuilder` is an explicit escape hatch (caller scopes
manually).
- Provided via Nest DI with
`@InjectWorkspaceScopedRepository(EntityClass)` and the
`provideWorkspaceScopedRepository(EntityClass)` provider factory.
- 19 unit tests cover the merge behavior, override-on-conflict, and the
array-where (OR) case.

### Lint enforcement
(`packages/twenty-oxlint-rules/rules/prefer-workspace-scoped-repository.ts`)
- New `twenty/prefer-workspace-scoped-repository` rule (level:
**error**).
- Blacklist of entity names: raw `@InjectRepository(E)` is rejected if
`E` is on the list.
- Initial list: `AgentTurnEntity`, `AgentMessageEntity`,
`AgentMessagePartEntity`, `AgentChatThreadEntity`,
`AgentTurnEvaluationEntity`, `AgentEntity`.
- Designed to grow over time as more consumers are migrated.
- 5 rule tests.

### Migration in this PR
All consumers of the six blacklisted entities, including:
- AI agent / chat / monitor resolvers, services, and jobs
- `AgentService`, `AiAgentRoleService`, `AiAgentWorkflowAction`,
`ApplicationService`, `WorkspaceFlatAgentMapCacheService`
- Admin-panel chat (migrated where the lookup is workspace-known; one
documented `eslint-disable` on the threadId-discovery lookup that
necessarily precedes the `allowImpersonation` permission check)
- `AiAgentRoleService` unit spec updated to mock the scoped wrapper

## Future work (deliberately not in this PR)

A standalone audit identified ~14 additional `core`/`metadata` entities
with `workspaceId` that currently use raw `@InjectRepository` and could
be added to the blacklist. Notable candidates: `UserWorkspaceEntity` (42
sites), `AppTokenEntity` (10), `FileEntity` (7),
`BillingCustomerEntity`/`BillingSubscriptionEntity` (~22 combined). Each
should be its own PR — the migration is mechanical but the surface is
wide.

## Test plan
- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx lint twenty-server` — 0 warnings, 0 errors
- [x] `npx jest workspace-scoped-repository` — 19/19 pass
- [x] `npx nx test twenty-oxlint-rules` — 215/215 pass
- [x] `npx jest src/engine/metadata-modules/ai` — 44/44 pass
- [ ] Manual smoke: end-to-end AI agent chat send/receive (reviewer)
- [ ] Manual smoke: AI agent monitor — list turns, run evaluation
(reviewer)
- [ ] Manual smoke: admin-panel chat thread inspection (reviewer)
2026-05-27 18:52:53 +02:00
Clive F 46e7f23df1 fix(contact-creation): handle common email display-name shapes when auto-creating People (#20639)
## Summary

When messages are imported, Twenty auto-creates a Person record for any
recipient that doesn't exist yet. The display-name parser used at that
point is `displayName.split(' ')[0] / [1]`, which silently mangles
several common header shapes:

| Header | Old result |

|-------------------------------------------------|-----------------------------------------|
| `"Doe, John" <...>` | `firstName="Doe,"`, `lastName="John"` |
| `"John.Doe Doe" <...>` | `firstName="John.Doe"`, `lastName="Doe"`|
| `"Mary Jane Watson" <...>` | `lastName="Jane"` ("Watson" dropped) |
| `"john.doe@x.com" <john.doe@x.com>` (forwarder) | full address in
`firstName` |
| `"Doe, John:GROUP" <...>` (group-tag servers) |
`firstName="John:GROUP"` |

This PR rewrites `getFirstNameAndLastNameFromHandleAndDisplayName` to
handle each pattern. Behaviour in order:

1. Trim + strip wrapping quotes
2. Swap `"Last, First"` comma form
3. Fall back to handle parsing when display name contains `@` (real
names don't)
4. Split single dotted tokens (`"john.doe"` → `"John"`, `"Doe"`)
5. Preserve multi-word last names (`tokens.slice(1).join(' ')`)
6. De-synthesize dot-glued first names (`"John.Doe Doe"` → `"John"`,
`"Doe"`)
7. Strip `:XXX` trailing tag suffix from each parsed field

## Test plan

- [x] 16 new unit test cases covering each shape
(`__tests__/get-first-name-and-last-name-from-handle-and-display-name.util.spec.ts`)
- [x] Lint + typecheck clean
- [ ] No regression in the messaging import flow

---------

Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
2026-05-27 15:49:46 +00:00
Etienne eda41b4eba feat(ai) - add observability (#20850)
**AI Chat - Tool Executions (counters, tagged with model)**
ai-chat/tool-execution-succeeded: number of tool calls invoked by the AI
that completed without error
ai-chat/tool-execution-failed: number of tool calls invoked by the AI
that threw an error
**AI Chat - Token Usage (counters, tagged with model)**
ai-chat/input-tokens: total input tokens sent to the model across all
turns
ai-chat/output-tokens: total output tokens generated by the model
ai-chat/cache-read-tokens: input tokens served from the model's prompt
cache (cheaper)
ai-chat/cache-write-tokens: input tokens written into the prompt cache
for future reuse
**AI Chat - Latency (histograms in ms, tagged with model)**
ai-chat/turn-latency-ms: total duration of a full chat turn (from stream
start to stream end)
ai-chat/step-latency-ms: duration of a single reasoning/tool-call step
within a turn
ai-chat/ttft-ms: time-to-first-token, i.e. how long until the model
starts streaming output
**MCP - Tool Executions (counters)**
mcp/tool-execution-succeeded: number of MCP tool calls that completed
successfully
mcp/tool-execution-failed: number of MCP tool calls that threw an error
2026-05-22 15:32:51 +00:00
neo773 4554dbe3c9 make connectedAccount clients self contained (#20827)
Replace OAuth2ClientManagerService with per provider each loading their
own entity and resolving tokens internally

Removes the ugly spread pattern of sprinkling tokens everywhere, this
caused downtime of messaging when we migrated to encrypted tokens
2026-05-22 14:17:07 +00:00
Paul Rastoin 76e144e85a Deprecate messageChannel messageFolder calendarChannel standard objects (#20836)
# Introduction

Removing old standard objects `messageChannel` and `messageFolder` and
`calendarChannel`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 13:40:53 +00:00
neo773 323e66433e lint: migrate prettier to oxfmt (#20783)
Most changes are `implements` being unwrapped this is not a oxfmt
regression
Prettier in 3.7 (we're on 3.1) changed this behaviour prettier blog
[post](https://prettier.io/blog/2025/11/27/3.7.0#change-18094)

This unifies our linting tooling

---------

Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-22 00:21:33 +02:00
neo773 1e2ae5342b Send Email UI IMAP/SMTP message threading fix (#20784)
**Problem:**

When using Twenty Send Email UI IMAP/SMTP message threading is broken on
Twenty side as well as recipient email client

**Twenty side fix:**

- SMTP has no concept of `externalThreadId` sendEmail resolver always
returns null, this breaks threading
Fix is to pass `parentThreadExternalId` to
`resolveOutboundThreadExternalId` for SMTP/IMAP path

**Recipient email client fix:**

- Fetch associated threads as per RFC spec to write `References` header
 ```
From: johndoe@domain.com
To: janedoe@domain.com
Subject: Test
References: <root@...> <mid1@...> <parent@...>
```
2026-05-21 19:16:27 +00:00
Paul Rastoin 9b9c97a049 Deprecate and backfill delete ConnectedAccount twenty standard object (#20752)
# Introduction
Following connected account permissions refactor and encryption
Removing the old workspace schema twenty standard application
connectedAccount objects and related standard fields and index

- a lot of deadcode
- instance command backfill cleaning the connected account object from
workspaces
2026-05-21 13:42:09 +02:00
neo773 b869107a22 fix(messaging): preserve all gmail to/cc/bcc recipients as participants (#20491)
As title but I also refactored it a little to match our current file and
code conventions since the code was very old

Reported by a cloud customer

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-20 15:40:29 +00:00
Abdul Rahman c800eccc65 Slack workflow connector (#20427)
https://github.com/user-attachments/assets/5a746414-988b-473c-9401-b8863a3e1c15


https://github.com/user-attachments/assets/0cdebdb1-f7c8-43cb-beef-f279387b6ce9



https://github.com/user-attachments/assets/df31c631-0781-42d8-8e6e-e5a16573ee3b



https://github.com/user-attachments/assets/6adaeae4-f3c9-4a5f-b0df-50c1f9a78428

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-20 10:21:22 +00:00
Paul Rastoin 57f13c9b92 [CONNECTED_ACCOUNT_BREAKING_CHANGE] Encrypt ConnectedAccount connectionParameters (#20673)
# Introduction
Prevent any cross user `connectedAccount` `connectionParamaters` leak
Also encrypt in db all `connectionParameters` password
Never return any password through `DTO` anymore
The settings now allow update mutation without providing the password in
edition mode

Verified all `connectionParameters.password` interaction

## Integration tests
- Added more coverage for both failing and successful paths
- Introduced a new env var that allow bypass the provider connection
test

## Legacy connected Account decryption support
Stop allowing non encrypted decryption on `accessToken` and
`refreshToken`, only allow legacy decryption on refactored
`connectionParameters`

## Upsert ownership
Completely got rid of the connected workspace schema context which is
legacy
Also now a user can only upsert a connected account for him only..

## New UI
<img width="1770" height="1852" alt="image"
src="https://github.com/user-attachments/assets/55c1dc89-42ff-4084-95e2-cc5f9e23753b"
/>
If in edition the password is by default disabled
It needs to be selected as being edited to be enabled

## Next
- Refactor tool permissions flag not to include connected accounts
- Remove the legacy connected standard object
- Refactor and improve connected account resolver auth
2026-05-19 12:56:44 +00:00
neo773 6cd069ce40 messaging minor perf improvement (#20687)
This PR adds two changes

1. Pass `lite:true` to `ExecuteInWorkspaceContextOptions` introduced in
https://github.com/twentyhq/twenty/pull/18376

2. Remove redundant gmail alias call, it adds 300ms every cron job, we
only do it once now when user connects, realistically I don't see people
changing their aliases every day you only set it up once

actual real diff is small, it's just prettier format contributing to
diff

Objective decrease total time take per job
2026-05-19 10:38:34 +00:00
Charles Bochet e0b4c9918b feat(twenty-server): introduce ENCRYPTION_KEY env var with versioned envelope (#20528)
## Summary

- Adds `ENCRYPTION_KEY` (primary) and `FALLBACK_ENCRYPTION_KEY`
(decrypt-only fallback for rotation) env vars to twenty-server, with
backward-compatible fallback to `APP_SECRET` when `ENCRYPTION_KEY` is
unset.
- Introduces a versioned ciphertext envelope `enc:v2:<keyId>:<base64>`
using AES-256-GCM with HKDF-SHA256 derived per-context keys. The 8-hex
`keyId` fingerprint lets every row identify which physical key encrypted
it, so rotation routes directly to primary or fallback without trial
decryption; GCM's auth tag gives true integrity (legacy CTR has none).
- Migrates `ConnectedAccountTokenEncryptionService` to the new envelope
and plumbs `workspaceId` through every caller, so per-workspace HKDF
context binds each row to its tenant.

The remaining encryption sites (`jwt-key-manager`, `config-storage`,
`postgres-credentials`, `application-variable`, TOTP) stay on the legacy
unprefixed CTR path and will be migrated in follow-up PRs. The
operator-facing rotation runbook is out of scope here.

### Format details

`enc:v{N}:{keyId}:{base64}` — `N=2` is the only version produced by new
writes (`v1` exists for backward-compatible decryption of existing
connected-account rows). `keyId =
sha256(rawKey).slice(0,4).toString('hex')`. The CHECK constraint on
`core.connectedAccount.{accessToken,refreshToken}` is relaxed from `LIKE
'enc:v1:%'` to `LIKE 'enc:v_:%'` so both versions pass.

### Key resolution

| `ENCRYPTION_KEY` | `FALLBACK_ENCRYPTION_KEY` | `APP_SECRET` | Encrypt
with | Decrypt try order |
|---|---|---|---|---|
| set | set | (any) | `ENCRYPTION_KEY` | match `keyId` → primary →
fallback |
| set | unset | (any) | `ENCRYPTION_KEY` | match `keyId` → primary |
| unset | set | set | `APP_SECRET` | match `keyId` → `APP_SECRET` →
fallback |
| unset | unset | set | `APP_SECRET` | match `keyId` → `APP_SECRET` |
| unset | unset | unset | startup error | n/a |

## Test plan

- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx jest
'secret-encryption|connected-account-token-encryption|connected-account-refresh-tokens|encrypt-connected-account-tokens|connection-provider-oauth-flow'`
— 87 tests pass
- [x] New `secret-encryption.service.versioned.spec.ts` covers: key
resolution table (no-key error, APP_SECRET fallback, ENCRYPTION_KEY
precedence), v2 round-trip with/without workspaceId, GCM tamper
rejection, workspaceId-mismatch rejection, keyId-based primary→fallback
routing, missing-key error names the fingerprint, v1 legacy decryption,
no-prefix legacy decryption, malformed envelope rejection.
- [x] Updated `connected-account-token-encryption.service.spec.ts`
covers workspaceId binding and HKDF context isolation.
- [x] Updated slow instance command spec verifies workspaceId is
threaded through encryption and the relaxed `enc:v_:%` LIKE pattern
matches both v1 and v2.
- [ ] Manual E2E: connect a Gmail account on a freshly deployed instance
with `APP_SECRET` only → confirm `core.connectedAccount.accessToken` is
`enc:v2:<keyId>:<base64>`.
- [ ] Manual E2E: rotate — set `ENCRYPTION_KEY=<new>` and
`FALLBACK_ENCRYPTION_KEY=<old APP_SECRET>`, restart, confirm
pre-rotation rows still decrypt and new rows carry the new `keyId`.
- [ ] Manual E2E: missing key — set `ENCRYPTION_KEY=<new>` without the
fallback, confirm decrypt error names the old `keyId` so the operator
can identify the missing key.
2026-05-13 16:15:54 +00:00
David Farah aec2e01662 fix(server): handle ImapFlow socket errors instead of crashing the process (#20510)
## Summary

`ImapFlow` is an `EventEmitter`; per Node.js semantics, an emitted
`'error'` event with no listener becomes an uncaught exception that
exits the process. Both ImapFlow construction sites in `twenty-server`
(`ImapClientProvider` used by all messaging flows, and
`testImapConnection` in the connection-wizard validator) currently build
the client without attaching a permanent `'error'` listener, so a
transient socket condition (idle timeout, network blip, server-side
disconnect) crashes `twenty-server` and triggers a container restart
with a ~1 min HTTP 502 window for end users.

This patch attaches an `'error'` listener at each call site that logs
the error and lets `imapflow`'s internal reconnect handle recovery. Same
shape / same precedent as #20143 (Redis session-store client) which
fixed #20144.

Closes #20509.

## What changed

-
`packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider.ts`:
`ImapClientProvider.createConnection` now attaches `client.on('error',
...)` between construction and `connect()`.
-
`packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts`:
`testImapConnection` does the same on its short-lived test client.

Both listeners log via the existing `Logger` instance (matching the
resolver-level logging already in `ImapClientProvider.getClient`) and
surface `error.stack` so transient socket conditions are observable but
no longer fatal.

## Crash this fixes (real production stack)

```
node:events:487
      throw er; // Unhandled 'error' event
      ^

Error: Socket timeout
    at TLSSocket.<anonymous> (/app/node_modules/imapflow/lib/imap-flow.js:795:29)
    at TLSSocket.emit (node:events:509:28)
    at Socket._onTimeout (node:net:610:8)
    ...
Emitted 'error' event on ImapFlow instance at:
    at ImapFlow.emitError (/app/node_modules/imapflow/lib/imap-flow.js:397:14)
  code: 'ETIMEOUT',
```

End-user impact: server process exits cleanly (code 0), Docker / k8s
restarts it; the DB, worker, redis, and caddy containers are unaffected
— only the API server dies, taking the GraphQL/REST surface offline for
a ~1 min health-check warmup.

## Test plan

- [x] `npx nx typecheck twenty-server` (planned — relying on CI for
verification)
- [x] `npx nx lint:diff-with-main twenty-server` (planned — relying on
CI for verification)
- [x] Manually reproduced the crash on `v2.2` by hitting an
IMAP/SMTP/CalDAV outbound flow with Gmail; with the patch applied
locally to the running container (verified the listener fires and logs
without process exit), the server stays up across the same trigger
sequence.
- [ ] Unit-level coverage: behavior is "listener exists, doesn't throw"
— not easily covered without a contrived socket-mock test. Existing call
sites have no unit tests today; happy to add one if a reviewer prefers,
otherwise mirroring the convention from #20143 which merged without a
new test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: neo773 <neo773@protonmail.com>
2026-05-13 15:13:55 +00:00
Paul Rastoin a159a68e2c [twenty-server] no floating promises lint rule (#20499)
## Introduction

That's an audit + RFC

## Fire-and-forget (`void`) -- Intentional, correct

These are telemetry, metrics, and audit logging in hot paths or
non-critical contexts. `void` is the right choice.

| File | What was voided |
|---|---|
| `sign-in-up.service.ts` | `metricsService.incrementCounter` (sign-up
metric) + `auditService.insertWorkspaceEvent` (workspace created) |
| `use-graphql-error-handler.hook.ts` | 5x
`metricsService.incrementCounter` (GraphQL operation metrics) |
| `bullmq.driver.ts` | 2x `metricsService.incrementCounter` (job
completed/failed metrics) |
| `call-webhook.job.ts` | 2x `auditService.insertWorkspaceEvent` + 1x
`metricsService.incrementCounter` |
| `custom-domain-manager.service.ts` | `analytics.insertWorkspaceEvent`
(domain activation event) |
| `logic-function-executor.service.ts` |
`auditService.insertWorkspaceEvent` (function execution) |
| `workflow-runner.workspace-service.ts` |
`metricsService.incrementCounter` (throttle metric) |
| `cleaner.workspace-service.ts` | `metricsService.incrementCounter`
(deleted workspace metric) |
| `stream-agent-chat.job.ts` | Detached IIFE for streaming chunks
(intentional concurrent pipeline) |
| `workspace-auth-context.middleware.ts` |
`withWorkspaceAuthContext(...)` (AsyncLocalStorage, returns void anyway)
|

## Top-level script entry points (`void bootstrap()`)

These are module-level calls where the promise has no consumer. `void`
makes the lint rule happy and documents the intent.

| File | What changed |
|---|---|
| `main.ts` | `void bootstrap()` |
| `command.ts` | `void bootstrap()` |
| `queue-worker.ts` | `void bootstrap()` |
| `truncate-db.ts` | `void dropSchemasSequentially()` |
| `codegen/index.ts` | `void generateTests(forceArg)` |

## Now properly awaited -- Real bug fixes

These were floating promises that could silently fail, lose data, or
cause race conditions.

| File | What was fixed |
|---|---|
| `billing-sync-plans-data.command.ts` | `meters.map(async ...)` wrapped
in `Promise.all` -- was returning before upserts finished |
| `cache-storage.service.ts` | `setAdd` and `setPop` had `.then()`
chains that weren't returned/awaited |
| `create-audit-log-from-internal-event.ts` | 4x
`auditService.createObjectEvent` now awaited inside a job |
| `cleaner.workspace-service.ts` | 2x `emailService.send(...)` now
awaited -- emails could silently fail |
| `agent-async-executor.service.ts` | `calculateAndBillUsage` +
`billNativeWebSearchUsage` in `finally` block now awaited |
| `repair-tool-call.util.ts` | `calculateAndBillUsage` now awaited |
| `agent-title-generation.service.ts` | `calculateAndBillUsage` now
awaited |
| `chat-execution.service.ts` | `billNativeWebSearchUsage` now awaited |
| `ai-generate-text.controller.ts` | `calculateAndBillUsage` in
`finally` block now awaited |
| `agent-turn.resolver.ts` | `messageQueueService.add(...)` now awaited
|
| `command.ts` | `app.close()` now awaited (was exiting before graceful
shutdown) |
| `i18n.service.ts` | `loadTranslations()` in `onModuleInit` now awaited
|
| `workspace-query-hook.explorer.ts` | `explore()` in `onModuleInit` now
awaited |
| `message-queue.explorer.ts` | `handleProcessorGroupCollection` in
`onModuleInit` now awaited |
| `ai-billing.service.spec.ts` | Test now properly `await`s the async
call |
| `messaging-messages-import.service.spec.ts` | `expect(...)` now
properly `await`ed for async assertion |
| `archive.finalize()` (3 files) | Voided -- promise resolution already
handled by `pipeline()` / `on('end')` |

## Impersonation & security audit trail -- Upgraded from `void` to
`await`

These were previously fire-and-forget but are
security/compliance-critical events that must be reliably persisted.

| File | What was fixed |
|---|---|
| `impersonation.service.ts` | 4x `auditService.insertWorkspaceEvent`
now awaited (impersonation attempt, token generation
attempt/success/failure) |
| `auth.resolver.ts` | 5x `auditService.insertWorkspaceEvent` now
awaited (impersonation token exchange attempt/success/failure at server
and workspace levels) |
| `auth.service.ts` | 2x `analytics.insertWorkspaceEvent` now awaited
(impersonation attempted/issued) |

## Billing audit -- Upgraded from `void` to `await`

Payment events should be reliably persisted for financial/compliance
reporting.

| File | What was fixed |
|---|---|
| `billing-webhook-invoice.service.ts` |
`auditService.insertWorkspaceEvent(PAYMENT_RECEIVED_EVENT)` now awaited
inside Stripe webhook handler |

## Fire-and-forget with proper error handling -- Upgraded from bare
`void`

These remain non-blocking but now catch and log errors instead of
risking unhandled rejections.

| File | What was fixed |
|---|---|
| `logic-function-executor.service.ts` |
`applicationLogsService.writeLogs` now uses `.catch()` instead of bare
`void` -- user-facing logs should surface errors |

## Systemic infrastructure fixes

| File | What was fixed |
|---|---|
| `metrics.service.ts` | `incrementCounter`: Redis cache write
(`metricsCacheService.updateCounter`) now uses `.catch()` internally
instead of raw `await` -- prevents unhandled rejections across all `void
metricsService.incrementCounter(...)` call sites when Redis is unhealthy
|
| `audit.service.ts` | `preventIfDisabled`: made properly `async` with
`await` and consistent `Promise<{ success: boolean }>` return type.
Removed broken `catch` that returned an `AuditException` as a value
(wrong constructor args, unreachable dead code). Removed unused
`AuditException` import |

## Fixed in this session (beyond original PR)

| File | What changed |
|---|---|
| `telemetry.listener.ts` | Removed misleading `Promise.all` + `void`
combo; replaced with simple `for...of` + `void` |
| `message-queue.explorer.ts` | Changed from `void` to `await` so
startup crashes on registration failure |
2026-05-13 10:08:42 +00:00
neo773 b03f044d0f 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>
2026-05-11 19:42:51 +00:00
Paul Rastoin 6c18bacb93 Encrypt connected account accessToken and refreshToken (#20441)
# Introduction
Encrypt the `connectedAccount` `accessToken` and `refreshToken` using
`APP_SECRET` in order to mitigate potential data leak or `core` table
compromise

## Decrypt
Temporary allow already plain text stored token to be retrieve without
decryption until the slow instance has been passed
Will uncomment the invariant check in a patch when the instance slow has
fully be run

## Standards
- Token are encrypted as quickly as possible
- A token cannot be written in database non encrypted by mistake using a
custom constraint ( `enc:` prefix )

## What's next
We should standardize not managing secret as is in the the services and
layer, they should be encrypted on the flight the earliest and should
never be logged
Will create a dedicated pattern afterwards for `applicationVariables`
secrets too

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-05-11 15:16:12 +00:00
martmull e72a907baa Stop rejecting application token on calendar and message events requests (#20440)
fixes https://github.com/twentyhq/twenty/issues/20423 by authorizing
application token to perform calendarEvents and message queries
2026-05-11 15:24:24 +02:00
Félix Malfait 4da8878697 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>
2026-05-09 11:00:57 +02:00
martmull 0d05788547 Protect sendEmail endpoint and thread user context through logic function executor (#20369)
- Thread userId and userWorkspaceId through
LogicFunctionExecutorService.execute() so
application access tokens carry user context when available. This allows
logic functions
triggered by authenticated HTTP routes to call sendEmail with proper
user identity, making
  the existing verifyOwnership() check work naturally.
  - Gate the sendEmail resolver with
SettingsPermissionGuard(PermissionFlagType.SEND_EMAIL_TOOL) instead of
NoPermissionGuard,
ensuring only callers with the SEND_EMAIL_TOOL permission can send
emails.
2026-05-08 10:03:39 +00:00
neo773 8d001eb33f fix: don't mark IMAP channel as failed on transient server errors (#20273)
Map RFC 5530 codes to `TEMPORARY_ERROR` so sync retries instead of
terminally flagging `FAILED_INSUFFICIENT_PERMISSIONS` when the server is
briefly unavailable.

prod Logs

```
	2026-05-05 03:53:42.129	
    authenticationFailed: true
	2026-05-05 03:53:42.129	
    serverResponseCode: 'UNAVAILABLE',
	2026-05-05 03:53:42.129	
    responseText: 'Account is temporarily unavailable.',
	2026-05-05 03:53:42.129	
	2026-05-05 03:53:42.129	
    response: '2 NO [UNAVAILABLE] Account is temporarily unavailable.',
	2026-05-05 03:53:42.129	
  cause: Error: Command failed
	2026-05-05 03:53:42.129	
  code: 'INSUFFICIENT_PERMISSIONS',
Caused by: Error: Command failed

[Nest] 35  - 05/04/2026, 10:23:42 PM   ERROR [ImapGetAllFoldersService] MessageImportDriverException: IMAP authentication error: Command failed
```
2026-05-05 14:30:57 +02:00
neo773 a3f2fafce6 fix smtp outbound persist message (#20276)
`APPEND` used display name `Sent` instead of `INBOX.Sent`
Fix is to use mailbox path, extreacted this as a utility, all services
are consistent now.

/closes #20267
2026-05-05 14:28:03 +02:00
Félix Malfait 9e94045fa5 feat(apps): generic OAuth provider support for app SDK (#20181)
## Summary

App developers can now declare third-party OAuth integrations (GitHub,
Linear, Slack, etc.) in their manifest and the platform handles the full
authorize → callback → token-exchange → refresh → injection lifecycle.
The dev writes ~10 lines of config and reads tokens via
`useOAuth('linear')` inside any logic function.

```ts
// app/src/oauth-providers/linear.ts
export default defineOAuthProvider({
  universalIdentifier: '...',
  name: 'linear',
  displayName: 'Linear',
  authorizationEndpoint: 'https://linear.app/oauth/authorize',
  tokenEndpoint: 'https://api.linear.app/oauth/token',
  scopes: ['read', 'write'],
  connectionMode: 'per-user',
  clientIdVariable: 'LINEAR_CLIENT_ID',
  clientSecretVariable: 'LINEAR_CLIENT_SECRET',
  tokenRequestContentType: 'form-urlencoded',
});

// app/src/logic-functions/handlers/...
const { accessToken } = useOAuth('linear'); // throws OAuthNotConnectedError if missing
```

## Architecture

- **Storage**: extends the existing `connectedAccount` table — new
nullable `applicationOAuthProviderId` FK + new `app` value on the
`ConnectedAccountProvider` enum. Existing Google/Microsoft flows are
untouched.
- **OAuth flow**: a single `/apps/oauth/authorize` +
`/apps/oauth/callback` controller pair handles every app provider. State
travels in a JWT signed via the existing `JwtWrapperService` (new
`APP_OAUTH_STATE` token type).
- **Token exchange**: goes through
`SecureHttpClientService.createSsrfSafeFetch()` (so an installed app
can't point `tokenEndpoint` at internal hosts).
- **Refresh**: piggybacks on the existing
`ConnectedAccountRefreshTokensService` dispatch — Google/Microsoft
drivers untouched, new app driver lives engine-side under
`application-oauth-provider/refresh/`.
- **Injection**: the executor injects refreshed tokens as env vars
(`OAUTH_<NAME>_ACCESS_TOKEN`, `_HANDLE`, `_SCOPES`, `_CONNECTED`); the
SDK helpers `useOAuth` / `useOptionalOAuth` read them.
- **Frontend**: auto-rendered "OAuth Connections" section under each
app's settings tab (no custom front component needed). App-managed
connections are filtered out of `/settings/accounts` so the
email/calendar page stays focused.
- **Disconnect**: best-effort revoke against the manifest's
`revokeEndpoint` before deleting the row.

## Reference app

`packages/twenty-apps/internal/twenty-linear/` exercises the full
pipeline:

- `defineOAuthProvider` for Linear
- `POST /linear/create-issue` and `GET /linear/teams` HTTP-route logic
functions
- Vitest tests for the handlers

## Tests

- 14 server-side Jest tests: token-exchange util (form-urlencoded vs
JSON, PKCE, error paths), flow service (authorize URL shape, state
binding, ConnectedAccount upsert on first/reconnect, per-workspace mode,
invalid state)
- 8 app-level Vitest tests: handler error paths, GraphQL request shape,
Linear error propagation
- All 4 packages clean: `npx nx lint:diff-with-main` and `npx tsc
--noEmit`

## Test plan

- [ ] Apply migration on a dev DB: `npx nx run
twenty-server:database:migrate:prod`
- [ ] Regenerate frontend types: `npx nx run
twenty-front:graphql:generate --configuration=metadata`
- [ ] Create a Linear OAuth app at
https://linear.app/settings/api/applications/new with redirect URI
`<SERVER_URL>/apps/oauth/callback`
- [ ] Deploy + install `twenty-linear` on a workspace, paste the Linear
client id/secret into the app's variables
- [ ] Click "Connect Linear" in the app's settings tab → complete OAuth
→ verify `connectedAccount` row created with `provider = 'app'`
- [ ] Trigger `POST /linear/create-issue` with a valid teamId → verify
issue lands in Linear
- [ ] Disconnect → verify the row is deleted and (if Linear's revoke
endpoint is configured in the manifest) the revoke call fires
- [ ] Verify `/settings/accounts` does NOT show the Linear connection —
it appears only under the Linear app's settings tab

## Out of scope (deliberately)

- **Cron + per-user providers**: a cron-triggered function with a
per-user OAuth provider currently returns `CONNECTED=false` (no user
context). The follow-up design is `useOAuthForUser(name,
userWorkspaceId)` paired with a `POST /apps/oauth/connection-token`
endpoint, deferred to keep this PR focused.
- **Token encryption at rest**: tokens stored as plain `varchar`
matching the existing Google/Microsoft pattern. Worth a separate
cross-cutting PR.
- **Manifest endpoint pinning**: a malicious app upgrade could change
`tokenEndpoint` silently. Same trust model as logic-function source code
(which already runs arbitrary server-side); worth tightening across the
whole upgrade pipeline rather than just OAuth.
- **CLI helpers** (`twenty oauth show-callback-url`, `twenty oauth
connect`): manual setup for v1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 11:26:34 +02:00
dependabot[bot] 5bdbfe651e chore(deps): bump postal-mime from 2.6.1 to 2.7.4 (#20150)
Bumps [postal-mime](https://github.com/postalsys/postal-mime) from 2.6.1
to 2.7.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postalsys/postal-mime/releases">postal-mime's
releases</a>.</em></p>
<blockquote>
<h2>v2.7.4</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4">2.7.4</a>
(2026-03-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add missing originalKey to Header type and Uint8Array to Attachment
content (<a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0">92cc91c</a>)</li>
<li>include originalKey in parsed headers output (<a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347">83521c8</a>)</li>
<li>preserve __esModule and .default in CJS build for bundler interop
(<a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048">1466910</a>)</li>
<li>prevent RFC 2047 encoded-word address fabrication (<a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3">844f920</a>)</li>
</ul>
<h2>v2.7.3</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3">2.7.3</a>
(2026-01-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>correct TypeScript type definitions to match implementation (<a
href="https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2">b225d7c</a>)</li>
</ul>
<h2>v2.7.2</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2">2.7.2</a>
(2026-01-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add null checks for contentType.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395">ad8f4c6</a>)</li>
<li>improve RFC compliance for MIME parsing (<a
href="https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d">e004c3a</a>)</li>
</ul>
<h2>v2.7.1</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1">2.7.1</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Add null checks for contentDisposition.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add">fd54c37</a>)</li>
</ul>
<h2>v2.7.0</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0">2.7.0</a>
(2025-12-22)</h2>
<h3>Features</h3>
<ul>
<li>add headerLines property exposing raw header lines (<a
href="https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb">c79a02a</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postalsys/postal-mime/blob/master/CHANGELOG.md">postal-mime's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4">2.7.4</a>
(2026-03-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add missing originalKey to Header type and Uint8Array to Attachment
content (<a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0">92cc91c</a>)</li>
<li>include originalKey in parsed headers output (<a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347">83521c8</a>)</li>
<li>preserve __esModule and .default in CJS build for bundler interop
(<a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048">1466910</a>)</li>
<li>prevent RFC 2047 encoded-word address fabrication (<a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3">844f920</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3">2.7.3</a>
(2026-01-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>correct TypeScript type definitions to match implementation (<a
href="https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2">b225d7c</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2">2.7.2</a>
(2026-01-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add null checks for contentType.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395">ad8f4c6</a>)</li>
<li>improve RFC compliance for MIME parsing (<a
href="https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d">e004c3a</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1">2.7.1</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Add null checks for contentDisposition.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add">fd54c37</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0">2.7.0</a>
(2025-12-22)</h2>
<h3>Features</h3>
<ul>
<li>add headerLines property exposing raw header lines (<a
href="https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb">c79a02a</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postalsys/postal-mime/commit/178f1ef0b1cd0047e1b8e690beabfec541b4daa7"><code>178f1ef</code></a>
chore(master): release 2.7.4 (<a
href="https://redirect.github.com/postalsys/postal-mime/issues/88">#88</a>)</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/1f7ba618d42d34b779157dfa33794cbae383a24d"><code>1f7ba61</code></a>
chore: bump devDependencies</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347"><code>83521c8</code></a>
fix: include originalKey in parsed headers output</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/b0d7b11550a2a3c65a52a2adf4f8281058023cab"><code>b0d7b11</code></a>
test: improve test coverage across codebase</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/ebc5ce619649d13ad72f4d12414f3e337a9e248c"><code>ebc5ce6</code></a>
refactor: simplify and clean up codebase</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048"><code>1466910</code></a>
fix: preserve __esModule and .default in CJS build for bundler
interop</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3"><code>844f920</code></a>
fix: prevent RFC 2047 encoded-word address fabrication</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/24dc6c64dfb43d89a8c8837ec941c96ebfa2c1fa"><code>24dc6c6</code></a>
test: update type check test with originalKey property</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0"><code>92cc91c</code></a>
fix: add missing originalKey to Header type and Uint8Array to Attachment
content</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/aa5baeafa6ffd093ab447c22d20e5da25051faff"><code>aa5baea</code></a>
docs: add link to full documentation site</li>
<li>Additional commits viewable in <a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postal-mime&package-manager=npm_and_yarn&previous-version=2.6.1&new-version=2.7.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-30 11:38:44 +00:00
Roland Rodriguez 95fee18126 fix(server): match IMAP \Noselect attribute case-insensitively (#20043)
## Summary

`ImapGetAllFoldersService.isMailboxSelectable` checked
`mailbox.flags?.has('\\Noselect')`, which is case-sensitive. Per [RFC
3501 §6.3.8](https://www.rfc-editor.org/rfc/rfc3501#section-6.3.8), IMAP
attribute names are case-insensitive — different servers spell the flag
differently:

| Server | Spelling |
|---|---|
| Dovecot | `\Noselect` |
| Stalwart | `\NoSelect` |
| Cyrus | `\Noselect` |
| RFC examples | `\NOSELECT` |

The previous check only caught Dovecot's spelling. On other servers,
virtual namespace placeholders (e.g. Stalwart's `Shared Folders` parent)
passed through `isMailboxSelectable` and got persisted as folders. When
`MessagingMessageListFetchJob` later ran, it issued `SELECT "Shared
Folders"`, the server correctly rejected it with `NO [NONEXISTENT]
Mailbox does not exist.`, and the entire message-list fetch failed for
the channel.

## Reproduction

1. Connect an IMAP account whose server advertises a `\NoSelect` (or
`\NOSELECT`) namespace placeholder. Stalwart Mail v0.15.x exhibits this
with shared mailboxes:
   ```
   * LIST (\NoSelect) "/" "Shared Folders"
   ```
2. The folder discovery job persists it as a syncable folder.
3. `MessagingMessageListFetchJob` runs and fails:
   ```
   IMAP: Error fetching message list: D0 SELECT "Shared Folders"
   responseStatus: NO  serverResponseCode: NONEXISTENT
   ```

## Fix

Iterate the flag set and lowercase-compare against `'\\noselect'`. This
matches every legal spelling without changing behavior for compliant
`\Noselect` clients.

```diff
 private isMailboxSelectable(mailbox: ListResponse): boolean {
-  return !mailbox.flags?.has('\\Noselect');
+  if (!mailbox.flags) return true;
+  for (const flag of mailbox.flags) {
+    if (flag.toLowerCase() === '\\noselect') return false;
+  }
+  return true;
 }
```

## Test plan

- [x] Existing `\Noselect` test cases (`should not issue STATUS against
a \Noselect folder`, parent-reference preservation, Sent-folder
exclusion) still pass — the lowercase comparison subsumes them.
- [x] New parameterized test covers `\Noselect`, `\NoSelect`,
`\NOSELECT`, `\noselect` spellings against a Stalwart-style `Shared
Folders` namespace placeholder, asserting Twenty does **not** issue
`STATUS` on the placeholder and does **not** include it in the
discovered folder set.

---------

Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
2026-04-27 11:58:02 +00:00
neo773 5b8804ff06 gmail extract body from deeply nested MIME parts (#19989)
/closes #19879
2026-04-27 11:56:50 +00:00
neo773 6545ca274c fix(messaging): refactor SentMessagePersistenceService (#20077)
refactored `SentMessagePersistenceService` to be thin wrapper over
`saveMessagesAndEnqueueContactCreation`
this fixes a bug where the old logic did not call match participants
causing messages to not show up
2026-04-27 11:56:41 +00:00
neo773 071980d511 Revert "fix compute folders to update util (#19749)" (#19921)
This reverts commit 64470baa1e
2026-04-21 08:02:24 +00:00
neo773 68746e22a0 Send Email Tool: Don't persist message on SMTP only connections (#19756)
Previously this blocked users who only had SMTP configured to send
outbound emails, this fixes it by making messageChannel and persist
layer conditional
2026-04-17 12:56:38 +00:00
Charles Bochet 4103efcb84 fix: replace slow deep-equal with fastDeepEqual to resolve CPU bottleneck (#19771)
## Summary

- Replaced the `deep-equal` npm package with the existing
`fastDeepEqual` from `twenty-shared/utils` across 5 files in the server
and shared packages
- `deep-equal` was causing severe CPU overhead in the record update hot
path (`executeMany` → `formatTwentyOrmEventToDatabaseBatchEvent` →
`objectRecordChangedValues` → `deepEqual`, called **per field per
record**)
- `fastDeepEqual` is ~100x faster for plain JSON database records since
it skips unnecessary prototype chain inspection and edge-case handling
- Removed the now-unnecessary `LARGE_JSON_FIELDS` branching in
`objectRecordChangedValues` since all fields now use the fast
implementation
2026-04-16 17:23:50 +02:00
neo773 64470baa1e fix compute folders to update util (#19749)
This regressed with the migration work

Type checker should've caught it, but didn't because of `Partial`
changed it to `Pick` instead to avoid future cases

/closes https://github.com/twentyhq/twenty/issues/19745
2026-04-16 09:09:15 +00:00
Marie bc28e1557c Introduce updateWorkspaceMemberSettings and clarify product (#19441)
## Summary

Introduces a dedicated **metadata** mutation to update **standard
(non-custom)** workspace member settings, moves profile-related UI to
use it, and aligns **workspace member** record permissions with the rest
of the CRM so users cannot escalate visibility via RLS by editing their
own member record.

## Product behaviour

### Profile and appearance (standard fields)

- Users can still update **their own** standard workspace member fields
that the product exposes in **Settings / Profile** (e.g. name, locale,
color scheme, avatar flow) via the new
**`updateWorkspaceMemberSettings`** mutation.
- The mutation returns a **boolean**; the app **merges** the updated
fields into local state so the UI stays in sync without refetching the
full workspace member record.
- **Locale** changes also keep **`userWorkspace`** in sync when a locale
is present in the payload (including from the workspace `updateOne` path
when applicable).

### Custom fields on workspace members

- The dedicated metadata mutation **rejects** any **custom** workspace
member field (and unknown keys). Those updates must go through the
normal **object** `updateOne` pipeline, which is subject to **object-
and field-level** permissions like other records. But since we don't
have object- and field-level permission configuration for system objects
yet, this permission is derived from Workspace member settings
permission.
- **Workspace member** is no longer exempt from ORM permission
validation for updates merely because it is a **system** object. Users
who **do not** have workspace member access (e.g. no **Workspace
members** settings permission and no equivalent broad settings access on
the role) **cannot** use `updateOne` on `workspaceMember` to change
**custom** (or other) fields on their own row—even though that row is
used for RLS predicates.
- This closes a path where someone could widen what they can see by
writing to fields that drive row-level rules.

### Who can change another member

- Updating **another** user’s workspace member still requires
**Workspace members** (or equivalent) settings permission, consistent
with admin tooling.
2026-04-14 16:29:00 +00:00
neo773 7dfc556250 refactor messaging jobs (#19626)
Cleans up the code quality by migrating from Raw SQL to TypeORM
entities. The previous implementation was necessary to do cross‑schema
table joins but since we've migrated to the core schema we don't need it
anymore.

- Also extracted `toIsoStringOrNull` to a utility it was duplicated
several times
- Moved `isThrottled` logic from job handler to cron enqueuer
2026-04-13 14:39:52 +00:00
Félix Malfait 67e7f05a68 feat: email attachments and open-in-app click action (#19485)
## Changes

### Email Attachments
- Added `EmailAttachmentsField` component for uploading and managing
email attachments
- New `useUploadEmailAttachment` hook for handling file uploads with
size validation
- New `UPLOAD_EMAIL_ATTACHMENT_FILE` mutation for backend file
persistence
- Integrated attachments into email composer with file validation
- Added `EmailRecipientLimits` constant to enforce max recipients (100)
on frontend

### Open in App Click Action
- New `useOpenEmailInAppOrFallback` hook to open emails in the in-app
composer
- Email fields now default to "Open in app" action instead of "Open as
link"
- New `SettingsDataModelFieldOnClickActionForm` support for
`OPEN_IN_APP` action
- Email secondary table cell button now offers in-app composer as
alternative action
- `AttachmentChip` component moved from advanced-text-editor to file
module for reuse

### Refactoring & New Utilities
- Extracted `useComposeEmailForTargetRecord` hook for consistent email
composer opening
- New `useResolveDefaultEmailRecipient` hook to resolve recipient based
on record type
- New `getPrimaryEmailFromRecord` utility for safe email field access
- New `EmptyInboxPlaceholder` component with CTA button
- Simplified `ComposeEmailButton` using new hooks
- Enhanced `ComposeEmailCommand` to support bulk Person selections
- Updated `useSendEmail` to accept and forward attachments
- Recipient count validation with warning in composer footer

### Backend
- New `FileEmailAttachmentModule` with resolver and service
- New `file-email-attachment.command` for record selection menu items
- Updated `SendEmailInput` GraphQL type to include `files` field
- Email tool constants and exceptions updated

### Type Updates
- Added `SendEmailAttachmentInput` GraphQL type
- Added `FileFolder.EMAIL_ATTACHMENT` to file folder interface

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-10 10:01:50 +02:00
Charles Bochet f6423f5925 Remove DataSourceService and clean up datasource migration logic (#19532)
## Summary

- **Drop the `objectMetadata.dataSourceId` foreign key and index** via a
1-22 fast instance command — column kept nullable for data preservation
- **Delete `DataSourceService`, `DataSourceModule`, and
`DataSourceException`** — all code now uses `workspace.databaseSchema`
directly
- **Remove `IS_DATASOURCE_MIGRATED` feature flag** from default flags
and all branching logic
- **Simplify workspace/object creation pipelines** —
`WorkspaceManagerService`, `DevSeederService`, and the object creation
action handler no longer route through `DataSourceService`
- **Keep `DataSourceEntity` and the `dataSource` table** for historical
data — entity stripped of all ORM relations
2026-04-10 07:34:05 +00:00
neo773 7bd84a6029 messaging fix relaunch cron jobs (#19492) 2026-04-09 12:25:12 +00:00
neo773 fda3eabb5f introduce MESSAGING_MESSAGES_GET_BATCH_SIZE as config variable (#19455)
makes`MESSAGING_MESSAGES_GET_BATCH_SIZE` configurable for self hosters

/closes #19147
2026-04-08 14:47:04 +00:00
neo773 20adb86917 fix: IMAP skip no-select flag folders properly (#19402)
Tested with a dovecot server running in Docker with synthetic seed

<img width="546" height="54" alt="image"
src="https://github.com/user-attachments/assets/81cbeae6-9cb6-406b-846a-209af403385f"
/>


/closes #19090
2026-04-08 04:54:01 +00:00
Félix Malfait f39fccc3c4 fix(messaging): split thread create/update statements and stop clobbering accumulator (#19388)
## Summary

Fixes two bugs in
`MessagingMessageService.saveMessagesWithinTransaction`.

### Bug 1 — `ON CONFLICT DO UPDATE command cannot affect row a second
time`

Reported in production logs from `MessagingMessagesImportService`.
Postgres rejects a single `INSERT … ON CONFLICT DO UPDATE` when the same
conflict target appears twice in the values list, and that's exactly
what was happening to `messageThread`.

Where it came from: #19351 added the message-thread subject refresh
feature and, in doing so, switched the existing thread `insert` to a
bulk `upsert(['id'])` over a list built by concatenating two sources:

```ts
const threadsToUpsert = [
  ...messageThreadsToCreate,        // brand-new thread rows
  ...threadSubjectUpdates entries,  // subject refreshes for existing threads
];
await messageThreadRepository.upsert(threadsToUpsert, ['id'], txManager);
```

Each list is internally unique, but they are **not disjoint**.
`enrichMessageAccumulatorWithMessageThreadToCreate`, when it sees two
messages in the same batch sharing a brand-new thread external id,
copies the first sibling's freshly-minted thread id into the second
sibling's `existingThreadInDB`. The subject-update gate later in the
loop then trusts that field and queues a subject refresh for that id —
which is also already in `messageThreadsToCreate`. Same id, same
statement, two rows → Postgres aborts the transaction and the import
retries forever on the same batch.

**Fix:** stop merging the two lists. Issue creates and subject updates
as two separate statements within the same transaction:

```ts
if (messageThreadsToCreate.length > 0) {
  await messageThreadRepository.insert(messageThreadsToCreate, txManager);
}
if (threadSubjectUpdates.size > 0) {
  await messageThreadRepository.upsert(
    Array.from(threadSubjectUpdates.entries()).map(([id, { subject }]) => ({ id, subject })),
    ['id'],
    txManager,
  );
}
```

This is closer to the pre-#19351 shape (`insert` for new rows) and
side-steps the duplicate-row constraint entirely: each statement is
internally unique (creates use freshly minted UUIDs; updates are keyed
by a `Map<id, …>`), and within the same transaction Postgres happily
applies a subject update to a row inserted by a previous statement.

### Bug 2 —
`enrichMessageAccumulatorWithExistingMessageChannelMessageAssociations`
clobbers the accumulator

Independent latent bug spotted while tracing the flow. The helper did:

```ts
if (existingMessageChannelMessageAssociation) {
  messageAccumulatorMap.set(message.externalId, {
    existingMessageInDB: existingMessage,
    existingMessageChannelMessageAssociationInDB: existingMessageChannelMessageAssociation,
  });
}
```

i.e. it **replaces** the accumulator object, dropping the
`existingThreadInDB` set just before by
`enrichMessageAccumulatorWithExistingMessageThreadIds`.

The branch only fires when re-encountering a message that's already been
fully synced on this channel (matched on `headerMessageId` AND already
has an association row) — i.e. routinely on Gmail/IMAP incremental syncs
whenever the connector re-delivers an existing message (label change,
read/unread, archive, full-resync after error, …).

When it fires, the next enrichment step sees `existingThreadInDB` as
`undefined`, falls into the "create a new thread" branch, mints a fresh
`threadToCreate`, but the main loop never queues a `messageToCreate` or
association for it (because both `existingMessageInDB` and the existing
association are still set). Net effect: **one orphan `messageThread` row
inserted per re-encountered message, with nothing referencing it.**

The existing message in the DB keeps pointing at its real thread, so
this is invisible to users — no thread fragmentation, no UI symptoms, no
error logs. Just slow accumulation of orphan thread rows that no query
joins onto. Probably worth running

```sql
SELECT COUNT(*)
FROM "messageThread" mt
WHERE NOT EXISTS (
  SELECT 1 FROM message m WHERE m."messageThreadId" = mt.id
);
```

on a busy production workspace once this lands to size whether a cleanup
migration is warranted.

**Fix:** mutate the existing accumulator in place instead of replacing
it.

## Test plan

- [x] `oxlint --type-aware` clean on touched file
- [x] `prettier` clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-07 13:23:31 +02:00
neo773 b23d2b4e73 messaging post migration cleanup (#19365)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 10:56:49 +02:00
Félix Malfait 83d30f8b76 feat: Send email from UI — inline reply composer & SendEmail mutation (#19363)
## Summary

- **Inline email reply**: Replace external email client redirects
(Gmail/Outlook deeplinks) with an in-app email composer. Users can reply
to email threads directly from the email thread widget or via the
command menu.
- **SendEmail GraphQL mutation**: New backend mutation that reuses
`EmailComposerService` for body sanitization, recipient validation, and
SMTP dispatch via the existing outbound messaging infrastructure.
- **Side panel compose page**: Command menu "Reply" action now opens a
side-panel compose email page with pre-filled To, Subject, and
In-Reply-To fields.

### Backend
- `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO`
- `SendEmailModule` wired into `CoreEngineModule`
- Reuses `EmailComposerService` + `MessagingMessageOutboundService`

### Frontend
- `EmailComposer` / `EmailComposerFields` components
- `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks
- `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage`
- `EmailThreadWidget` inline Reply bar with toggle composer
- `ReplyToEmailThreadCommand` now opens side-panel instead of external
links

### Seeds
- Added `handle` field to message participant seeds for realistic email
addresses
- Seed `connectedAccount` and `messageChannel` in correct batch order

## Test plan

- [ ] Open an email thread on a person/company record → verify
"Reply..." bar appears below the last message
- [ ] Click "Reply..." → composer opens inline with pre-filled To and
Subject
- [ ] Type a message and click Send → email is sent via SMTP, composer
closes
- [ ] Use command menu Reply action → side panel opens with compose
email page
- [ ] Verify Send/Cancel buttons work correctly in side panel
- [ ] Test with Cc/Bcc toggle in composer fields
- [ ] Verify error handling: invalid recipients, missing connected
account


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 08:43:48 +02:00
Félix Malfait 8acfacc69c Add email thread widget and message thread record page layout (#19351)
## Summary
- Move email thread display from side panel to a dedicated record page
with a new `EMAIL_THREAD` widget type
- Add message thread as a standard object with page layout, subject
field, and backfill command
- Add reply-to-email command menu item for message thread records
- Remove old side panel message thread components in favor of the new
widget-based approach

## Type fixes
- Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all
configuration/validator maps
- Create `EmailThreadConfigurationDTO` and shared
`EmailThreadConfiguration` type
- Register EMAIL_THREAD in widget type validators, configuration
resolvers, and standard widget mappings

## Test plan
- [ ] Verify message thread record pages render with the email thread
widget
- [ ] Verify email thread preview navigates to the record page instead
of opening side panel
- [ ] Verify reply-to-email command appears for message thread records
- [ ] Verify typecheck passes for both twenty-front and twenty-server
- [ ] Run existing test suites to check for regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 12:02:04 +02:00
neo773 d3f0162cf5 Remove connected account feature flag (#19286)
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-06 08:13:41 +00:00
Paul Rastoin cd23a2bc80 Cleaning UpgradeCommand code flow (#19241)
# Introduction
Currently preparing the `UpgradeCommand` refactor, in this way started
by cleaning up the existing avoiding unecessary dependencies to others
services allowing easier readability and concern centralization for
upcoming refactor
The UpgradeCommand was extending up to five classes, overriding
abstracted class and so on. It was also cascade
injecting 3 services

Introducing the `WorkspaceIteratorService` that centralize the commands
set to run over a single workspace logic shared between both atomic
upgrade command call and global upgradeCommand

## Tradeoff
Duplicated `@Option` between both `UpgradeCommandRunner` and
`WorkspaceMigrationRunner`


## Before
```
UpgradeCommand
  └─ extends UpgradeCommandRunner
       └─ extends ActiveOrSuspendedWorkspacesMigrationCommandRunner
            └─ extends WorkspacesMigrationCommandRunner  (owns workspace iteration loop + ORM deps)
                 └─ extends MigrationCommandRunner       (dry-run, verbose, error handling)
                      └─ extends CommandRunner            (nest-commander)
```

## Now

```
UpgradeCommand
  └─ extends UpgradeCommandRunner
       └─ extends CommandRunner                 (nest-commander)
       uses ─► Services         (via composition)
```

## Logging management
At the moment all services are logging, in the best of the world only
the runners should be doing so
2026-04-02 13:24:55 +00:00
neo773 2fccd29ec6 messaging cleanup (#19124) 2026-03-30 16:21:00 +02:00