From 72d728ea8e53b2353f56b13b41d4c243c56ecd4a Mon Sep 17 00:00:00 2001 From: Pratik Mahajan Date: Wed, 8 Jul 2026 10:59:25 -0400 Subject: [PATCH] fix(messaging): add sender name to IMAP/SMTP From headers (#22603) Manual IMAP/SMTP outbound emails were being composed with a bare email address in the From header, so recipients did not see the sender's display name. Gmail already built a proper sender header from Google profile data, but manually configured IMAP/SMTP accounts had no equivalent path and fell back to the raw address only. Fix this by storing the optional sender display name in the IMAP/SMTP/CALDAV connection parameters, exposing it through the settings flow and metadata API, and reusing a shared From-header formatter when composing outbound messages. The formatter now builds a properly encoded sender header when a name is available and falls back to the bare email address when it is not, keeping the behavior safe for blank or missing names. Gmail keeps using its existing Google-derived display name source; this change only brings manual accounts up to the same header formatting standard and removes duplicated formatting logic between outbound drivers. After this change, manual SMTP sends and IMAP draft creation include the configured sender name in the From header, while blank names are normalized away instead of producing malformed headers. Existing manual account names are preserved when updates omit the field, and edited accounts can still explicitly clear it through the settings flow. Add focused utility coverage for the shared From-header formatter. Fixes: #22608 Review in cubic --------- Co-authored-by: neo773 --- .../src/metadata/generated/schema.graphql | 2 ++ .../src/metadata/generated/schema.ts | 4 ++- .../src/metadata/generated/types.ts | 6 ++++ .../src/generated-metadata/graphql.ts | 6 ++-- .../SettingsAccountsConnectionForm.tsx | 16 ++++++++++ .../getConnectedImapSmtpCaldavAccount.ts | 1 + .../hooks/useImapSmtpCaldavConnectionForm.ts | 8 ++++- .../connectionImapSmtpCalDav.ts | 2 ++ .../imap-smtp-caldav-connected-account.dto.ts | 3 ++ .../dtos/imap-smtp-caldav-connection.input.ts | 3 ++ .../imap-smtp-caldav-connection.service.ts | 11 ++++++- .../types/imap-smtp-caldav-connection.type.ts | 1 + ...build-public-connection-parameters.util.ts | 5 +++- ...nected-account-token-encryption.service.ts | 8 +++-- .../gmail-message-outbound.service.ts | 13 +++++--- .../imap-smtp-message-outbound.service.ts | 23 ++++++++------ .../format-message-from-header.util.spec.ts | 30 +++++++++++++++++++ .../utils/format-message-from-header.util.ts | 15 ++++++++++ 18 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/__tests__/format-message-from-header.util.spec.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/format-message-from-header.util.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 89b7c6fb52..4d96fd83e3 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -2655,6 +2655,7 @@ type ImapSmtpCaldavPublicConnectionParams { } type ImapSmtpCaldavPublicConnectionParameters { + name: String IMAP: ImapSmtpCaldavPublicConnectionParams SMTP: ImapSmtpCaldavPublicConnectionParams CALDAV: ImapSmtpCaldavPublicConnectionParams @@ -4660,6 +4661,7 @@ input SendEmailAttachmentInput { } input EmailAccountConnectionParameters { + name: String IMAP: ConnectionParametersInput SMTP: ConnectionParametersInput CALDAV: ConnectionParametersInput diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 7f753e5233..6c41965cd7 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2314,6 +2314,7 @@ export interface ImapSmtpCaldavPublicConnectionParams { } export interface ImapSmtpCaldavPublicConnectionParameters { + name?: Scalars['String'] IMAP?: ImapSmtpCaldavPublicConnectionParams SMTP?: ImapSmtpCaldavPublicConnectionParams CALDAV?: ImapSmtpCaldavPublicConnectionParams @@ -5490,6 +5491,7 @@ export interface ImapSmtpCaldavPublicConnectionParamsGenqlSelection{ } export interface ImapSmtpCaldavPublicConnectionParametersGenqlSelection{ + name?: boolean | number IMAP?: ImapSmtpCaldavPublicConnectionParamsGenqlSelection SMTP?: ImapSmtpCaldavPublicConnectionParamsGenqlSelection CALDAV?: ImapSmtpCaldavPublicConnectionParamsGenqlSelection @@ -6673,7 +6675,7 @@ export interface SendEmailInput {connectedAccountId: Scalars['String'],to: Scala export interface SendEmailAttachmentInput {id: Scalars['String'],name: Scalars['String']} -export interface EmailAccountConnectionParameters {IMAP?: (ConnectionParametersInput | null),SMTP?: (ConnectionParametersInput | null),CALDAV?: (ConnectionParametersInput | null)} +export interface EmailAccountConnectionParameters {name?: (Scalars['String'] | null),IMAP?: (ConnectionParametersInput | null),SMTP?: (ConnectionParametersInput | null),CALDAV?: (ConnectionParametersInput | null)} export interface ConnectionParametersInput {host: Scalars['String'],port: Scalars['Float'],username?: (Scalars['String'] | null),password?: (Scalars['String'] | null),connectionSecurity?: (EmailConnectionSecurity | null)} diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 6918aa6cf7..2892301514 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -5228,6 +5228,9 @@ export default { ] }, "ImapSmtpCaldavPublicConnectionParameters": { + "name": [ + 1 + ], "IMAP": [ 293 ], @@ -11885,6 +11888,9 @@ export default { ] }, "EmailAccountConnectionParameters": { + "name": [ + 1 + ], "IMAP": [ 493 ], diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 7c486b397e..817a7320be 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1548,6 +1548,7 @@ export type EmailAccountConnectionParameters = { CALDAV?: InputMaybe; IMAP?: InputMaybe; SMTP?: InputMaybe; + name?: InputMaybe; }; export enum EmailConnectionSecurity { @@ -2052,6 +2053,7 @@ export type ImapSmtpCaldavPublicConnectionParameters = { CALDAV?: Maybe; IMAP?: Maybe; SMTP?: Maybe; + name?: Maybe; }; export type ImapSmtpCaldavPublicConnectionParams = { @@ -7724,7 +7726,7 @@ export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{ }>; -export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, userWorkspaceId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavPublicConnectionParameters', IMAP?: { __typename?: 'ImapSmtpCaldavPublicConnectionParams', host: string, port: number, connectionSecurity?: EmailConnectionSecurity | null, username?: string | null } | null, SMTP?: { __typename?: 'ImapSmtpCaldavPublicConnectionParams', host: string, username?: string | null, port: number, connectionSecurity?: EmailConnectionSecurity | null } | null, CALDAV?: { __typename?: 'ImapSmtpCaldavPublicConnectionParams', host: string, port: number, connectionSecurity?: EmailConnectionSecurity | null, username?: string | null } | null } | null } }; +export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, userWorkspaceId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavPublicConnectionParameters', name?: string | null, IMAP?: { __typename?: 'ImapSmtpCaldavPublicConnectionParams', host: string, port: number, connectionSecurity?: EmailConnectionSecurity | null, username?: string | null } | null, SMTP?: { __typename?: 'ImapSmtpCaldavPublicConnectionParams', host: string, username?: string | null, port: number, connectionSecurity?: EmailConnectionSecurity | null } | null, CALDAV?: { __typename?: 'ImapSmtpCaldavPublicConnectionParams', host: string, port: number, connectionSecurity?: EmailConnectionSecurity | null, username?: string | null } | null } | null } }; export type MyCalendarChannelsQueryVariables = Exact<{ connectedAccountId?: InputMaybe; @@ -8979,7 +8981,7 @@ export const UpdateCalendarChannelDocument = {"kind":"Document","definitions":[{ export const UpdateMessageChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}}]}}]}}]} as unknown as DocumentNode; export const UpdateMessageFolderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFolderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode; export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSyncAction"}}]}}]}}]} as unknown as DocumentNode; -export const GetConnectedImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectedImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConnectedImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetConnectedImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectedImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConnectedImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const MyCalendarChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyCalendarChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myCalendarChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const MyConnectedAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"authFailedAt"}},{"kind":"Field","name":{"kind":"Name","value":"archivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"handleAliases"}},{"kind":"Field","name":{"kind":"Name","value":"lastSignedInAt"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"lastCredentialsRefreshedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const MyMessageChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx index 6da5bef26d..439f1866ab 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectionForm.tsx @@ -97,6 +97,22 @@ export const SettingsAccountsConnectionForm = ({
+ ( + + )} + /> + { if (isDefined(connectedAccount)) { reset({ + name: connectedAccount.connectionParameters?.name || '', handle: connectedAccount.handle || '', IMAP: { ...DEFAULT_PROTOCOL_VALUES.IMAP, @@ -168,7 +171,10 @@ export const useImapSmtpCaldavConnectionForm = ({ ? { id: connectedAccountId } : {}), handle: formValues.handle, - connectionParameters, + connectionParameters: { + ...connectionParameters, + name: formValues.name, + }, }, }); if (!isDefined(data)) return; diff --git a/packages/twenty-front/src/modules/settings/accounts/validation-schemas/connectionImapSmtpCalDav.ts b/packages/twenty-front/src/modules/settings/accounts/validation-schemas/connectionImapSmtpCalDav.ts index 8e5667c943..685656a820 100644 --- a/packages/twenty-front/src/modules/settings/accounts/validation-schemas/connectionImapSmtpCalDav.ts +++ b/packages/twenty-front/src/modules/settings/accounts/validation-schemas/connectionImapSmtpCalDav.ts @@ -32,6 +32,7 @@ const connectionParameters = z export const connectionImapSmtpCalDav = z .object({ + name: z.string().trim(), handle: z.email('Invalid email address'), IMAP: connectionParameters.optional(), SMTP: connectionParameters.optional(), @@ -52,6 +53,7 @@ export const connectionImapSmtpCalDav = z export const connectionImapSmtpCalDavUpdate = z .object({ + name: z.string().trim(), handle: z.email('Invalid email address'), IMAP: connectionParameters.optional(), SMTP: connectionParameters.optional(), diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connected-account.dto.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connected-account.dto.ts index 7bd7ed6b8a..63f6cc306a 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connected-account.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connected-account.dto.ts @@ -23,6 +23,9 @@ class PublicConnectionParamsDTO { @ObjectType('ImapSmtpCaldavPublicConnectionParameters') class ImapSmtpCaldavPublicConnectionParametersDTO { + @Field(() => String, { nullable: true }) + name?: string | null; + @Field(() => PublicConnectionParamsDTO, { nullable: true }) IMAP?: PublicConnectionParamsDTO; diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.input.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.input.ts index a7ebc7e351..c05a3689f2 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.input.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.input.ts @@ -32,6 +32,9 @@ export class ConnectionParametersInput { @InputType('EmailAccountConnectionParameters') export class EmailAccountConnectionParametersInput { + @Field(() => String, { nullable: true }) + name?: string | null; + @Field(() => ConnectionParametersInput, { nullable: true }) IMAP?: ConnectionParametersInput; diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts index 8c9e279124..2f40a97a72 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts @@ -3,8 +3,9 @@ import { Injectable, Logger } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; import { ImapFlow } from 'imapflow'; import { createTransport } from 'nodemailer'; +import { isNonEmptyString } from '@sniptt/guards'; import { ACCOUNT_TYPES } from 'twenty-shared/constants'; -import { assertUnreachable } from 'twenty-shared/utils'; +import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { type EmailAccountConnectionParametersInput } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.input'; @@ -224,6 +225,14 @@ export class ImapSmtpCaldavService { }): Promise { const validatedParams: PlaintextImapSmtpCaldavParams = {}; + if (isDefined(connectionParameters.name)) { + const trimmedName = connectionParameters.name.trim(); + + validatedParams.name = isNonEmptyString(trimmedName) ? trimmedName : null; + } else if (isDefined(existingConnectionParameters?.name)) { + validatedParams.name = existingConnectionParameters.name; + } + for (const protocol of ACCOUNT_TYPES) { const params = connectionParameters[protocol]; diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type.ts index 37923774b6..18c0c101bd 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type.ts @@ -27,6 +27,7 @@ export type AccountType = (typeof ACCOUNT_TYPES)[number]; export { EmailConnectionSecurity } from 'src/engine/core-modules/imap-smtp-caldav-connection/enums/email-connection-security.enum'; export type ImapSmtpCaldavParams = { + name?: string | null; IMAP?: ConnectionParameters; SMTP?: ConnectionParameters; CALDAV?: ConnectionParameters; diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/utils/build-public-connection-parameters.util.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/utils/build-public-connection-parameters.util.ts index f0f566e27c..4f299cafa3 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/utils/build-public-connection-parameters.util.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/utils/build-public-connection-parameters.util.ts @@ -14,6 +14,7 @@ type PublicConnectionParams = { }; type PublicConnectionParameters = { + name?: string | null; IMAP?: PublicConnectionParams; SMTP?: PublicConnectionParams; CALDAV?: PublicConnectionParams; @@ -40,6 +41,8 @@ export const buildPublicConnectionParameters = ( return result; }, - {}, + { + name: connectionParameters.name ?? null, + }, ); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service.ts index c7d41aaf3b..106b40f63a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service.ts @@ -125,7 +125,9 @@ export class ConnectedAccountTokenEncryptionService { connectionParameters: PlaintextImapSmtpCaldavParams; workspaceId: string; }): EncryptedImapSmtpCaldavParams { - const result: EncryptedImapSmtpCaldavParams = {}; + const result: EncryptedImapSmtpCaldavParams = { + name: connectionParameters.name ?? null, + }; for (const protocol of ACCOUNT_TYPES) { const params = connectionParameters[protocol]; @@ -150,7 +152,9 @@ export class ConnectedAccountTokenEncryptionService { connectionParameters: EncryptedImapSmtpCaldavParams; workspaceId: string; }): PlaintextImapSmtpCaldavParams { - const result: PlaintextImapSmtpCaldavParams = {}; + const result: PlaintextImapSmtpCaldavParams = { + name: connectionParameters.name ?? null, + }; for (const protocol of ACCOUNT_TYPES) { const params = connectionParameters[protocol]; diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/gmail/services/gmail-message-outbound.service.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/gmail/services/gmail-message-outbound.service.ts index c1f8f34387..a2ac335749 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/gmail/services/gmail-message-outbound.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/gmail/services/gmail-message-outbound.service.ts @@ -9,10 +9,10 @@ import { type MessageOutboundDriver } from 'src/modules/messaging/message-outbou import { GoogleOAuth2ClientProvider } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client.provider'; import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; -import { mimeEncode } from 'src/modules/messaging/message-import-manager/utils/mime-encode.util'; import { type SendMessageInput } from 'src/modules/messaging/message-outbound-manager/types/send-message-input.type'; import { type SendMessageResult } from 'src/modules/messaging/message-outbound-manager/types/send-message-result.type'; import { extractMessageIdFromBuffer } from 'src/modules/messaging/message-outbound-manager/utils/extract-message-id-from-buffer.util'; +import { formatMessageFromHeader } from 'src/modules/messaging/message-outbound-manager/utils/format-message-from-header.util'; import { toMailComposerOptions } from 'src/modules/messaging/message-outbound-manager/utils/to-mail-composer-options.util'; @Injectable() @@ -169,6 +169,10 @@ export class GmailMessageOutboundService implements MessageOutboundDriver { const fromEmail = gmailData.emailAddress; + if (!isNonEmptyString(fromEmail)) { + throw new Error('Gmail profile did not return an email address'); + } + const { data: peopleData } = await peopleClient.people.get({ resourceName: 'people/me', personFields: 'names', @@ -176,9 +180,10 @@ export class GmailMessageOutboundService implements MessageOutboundDriver { const fromName = peopleData?.names?.[0]?.displayName; - const from = isDefined(fromName) - ? `"${mimeEncode(fromName)}" <${fromEmail}>` - : `${fromEmail}`; + const from = formatMessageFromHeader({ + fromEmail, + fromName, + }); const mail = new MailComposer( toMailComposerOptions(from, sendMessageInput), diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/imap/services/imap-smtp-message-outbound.service.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/imap/services/imap-smtp-message-outbound.service.ts index 13ff820410..697b5c5df2 100644 --- a/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/imap/services/imap-smtp-message-outbound.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/drivers/imap/services/imap-smtp-message-outbound.service.ts @@ -18,6 +18,7 @@ import { SmtpClientProvider } from 'src/modules/messaging/message-import-manager import { type SendMessageInput } from 'src/modules/messaging/message-outbound-manager/types/send-message-input.type'; import { type SendMessageResult } from 'src/modules/messaging/message-outbound-manager/types/send-message-result.type'; import { extractMessageIdFromBuffer } from 'src/modules/messaging/message-outbound-manager/utils/extract-message-id-from-buffer.util'; +import { formatMessageFromHeader } from 'src/modules/messaging/message-outbound-manager/utils/format-message-from-header.util'; import { toMailComposerOptions } from 'src/modules/messaging/message-outbound-manager/utils/to-mail-composer-options.util'; @Injectable() @@ -46,13 +47,15 @@ export class ImapSmtpMessageOutboundService implements MessageOutboundDriver { this.assertHandleIsDefined(handle); - const messageBuffer = await this.compileRawMessage( - handle, - sendMessageInput, - ); + const from = formatMessageFromHeader({ + fromEmail: handle, + fromName: connectionParameters?.name, + }); + + const messageBuffer = await this.compileRawMessage(from, sendMessageInput); await smtpClient.sendMail({ - from: handle, + from, to: sendMessageInput.to, cc: sendMessageInput.cc, bcc: sendMessageInput.bcc, @@ -108,10 +111,12 @@ export class ImapSmtpMessageOutboundService implements MessageOutboundDriver { throw new Error('IMAP connection is required to create drafts'); } - const messageBuffer = await this.compileRawMessage( - handle, - sendMessageInput, - ); + const from = formatMessageFromHeader({ + fromEmail: handle, + fromName: connectionParameters?.name, + }); + + const messageBuffer = await this.compileRawMessage(from, sendMessageInput); const imapClient = await this.imapClientProvider.getClient( connectedAccount.id, diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/__tests__/format-message-from-header.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/__tests__/format-message-from-header.util.spec.ts new file mode 100644 index 0000000000..049d3e7cf2 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/__tests__/format-message-from-header.util.spec.ts @@ -0,0 +1,30 @@ +import { formatMessageFromHeader } from 'src/modules/messaging/message-outbound-manager/utils/format-message-from-header.util'; + +describe('formatMessageFromHeader', () => { + it('should format name and email when name is provided', () => { + expect( + formatMessageFromHeader({ + fromEmail: 'user@example.com', + fromName: 'Test User', + }), + ).toBe('=?UTF-8?B?VGVzdCBVc2Vy?= '); + }); + + it('should fall back to the bare email when name is missing', () => { + expect( + formatMessageFromHeader({ + fromEmail: 'user@example.com', + fromName: null, + }), + ).toBe('user@example.com'); + }); + + it('should fall back to the bare email when name is blank', () => { + expect( + formatMessageFromHeader({ + fromEmail: 'user@example.com', + fromName: '', + }), + ).toBe('user@example.com'); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/format-message-from-header.util.ts b/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/format-message-from-header.util.ts new file mode 100644 index 0000000000..c5e95915d0 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-outbound-manager/utils/format-message-from-header.util.ts @@ -0,0 +1,15 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { mimeEncode } from 'src/modules/messaging/message-import-manager/utils/mime-encode.util'; + +export const formatMessageFromHeader = ({ + fromEmail, + fromName, +}: { + fromEmail: string; + fromName?: string | null; +}) => { + return isNonEmptyString(fromName) + ? `${mimeEncode(fromName)} <${fromEmail}>` + : fromEmail; +};