Compare commits

...

1 Commits

Author SHA1 Message Date
sonarly-bot 136c47eea0 fix: select email-capable account for Send Email fallback
https://sonarly.com/issue/42767?type=bug

The workflow Send Email step can fail when no account is explicitly selected, because fallback account selection can pick a non-email connected account and then crash the send path.

Fix: Implemented the root-cause fix in EmailComposer fallback selection:

- In `EmailComposerService`, replaced unfiltered “first connected account” fallback with email-capable account selection.
- Added `isEmailCapableConnectedAccount()` to gate fallback candidates to:
  - accounts with a message channel matching account handle, or
  - SMTP-only IMAP/SMTP/CALDAV accounts that actually have SMTP configuration.
- Updated fallback query to include `messageChannels` relation and deterministic ordering (`createdAt ASC`) before choosing first eligible account.
- If no email-capable account exists, now throws a clear `CONNECTED_ACCOUNT_NOT_FOUND` EmailToolException instead of selecting an incompatible account and failing later in compose/send.

This addresses the exact failure mode where empty/default `connectedAccountId` selected a non-mail account and then crashed with missing message channel.

Authored by Sonarly by autonomous analysis (run 48880).
2026-06-06 07:47:59 +00:00
4 changed files with 78 additions and 9 deletions
@@ -63,6 +63,10 @@ export class DraftEmailTool implements Tool {
};
} catch (error) {
if (error instanceof EmailToolException) {
this.logger.warn(
`Draft creation failed with a handled email tool error (${error.code}) in workspace ${context.workspaceId}`,
);
return {
success: false,
message: 'Failed to create draft',
@@ -70,9 +74,11 @@ export class DraftEmailTool implements Tool {
};
}
this.logger.error(`Failed to create draft: ${error}`);
if (isInsufficientPermissionsError(error)) {
this.logger.warn(
`Draft creation failed due to insufficient permissions in workspace ${context.workspaceId}`,
);
return {
success: false,
message: 'Failed to create draft due to insufficient permissions',
@@ -82,6 +88,11 @@ export class DraftEmailTool implements Tool {
};
}
this.logger.error(
`Failed to create draft in workspace ${context.workspaceId}`,
error instanceof Error ? error.stack : undefined,
);
return {
success: false,
message: 'Failed to create draft',
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { toPlainText } from '@react-email/render';
@@ -39,8 +39,6 @@ type ParentThreadContext = {
@Injectable()
export class EmailComposerService {
private readonly logger = new Logger(EmailComposerService.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectRepository(ConnectedAccountEntity)
@@ -87,6 +85,23 @@ export class EmailComposerService {
);
}
private isEmailCapableConnectedAccount(
connectedAccount: ConnectedAccountEntity,
): boolean {
const isSmtpOnlyAccount =
connectedAccount.provider ===
ConnectedAccountProvider.IMAP_SMTP_CALDAV &&
!isDefined(connectedAccount.connectionParameters?.IMAP);
if (isSmtpOnlyAccount) {
return isDefined(connectedAccount.connectionParameters?.SMTP);
}
return connectedAccount.messageChannels.some(
(messageChannel) => messageChannel.handle === connectedAccount.handle,
);
}
private async getOrThrowFirstConnectedAccountId(
workspaceId: string,
): Promise<string> {
@@ -96,6 +111,12 @@ export class EmailComposerService {
async () => {
const allAccounts = await this.connectedAccountRepository.find({
where: { workspaceId },
relations: {
messageChannels: true,
},
order: {
createdAt: 'ASC',
},
});
if (!allAccounts || allAccounts.length === 0) {
@@ -105,7 +126,18 @@ export class EmailComposerService {
);
}
return allAccounts[0].id;
const firstEmailCapableAccount = allAccounts.find((connectedAccount) =>
this.isEmailCapableConnectedAccount(connectedAccount),
);
if (!isDefined(firstEmailCapableAccount)) {
throw new EmailToolException(
'No email-capable connected accounts found for this workspace',
EmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return firstEmailCapableAccount.id;
},
authContext,
);
@@ -70,6 +70,10 @@ export class SendEmailTool implements Tool {
};
} catch (error) {
if (error instanceof EmailToolException) {
this.logger.warn(
`Email send failed with a handled email tool error (${error.code}) in workspace ${context.workspaceId}`,
);
return {
success: false,
message: 'Failed to send email',
@@ -77,9 +81,11 @@ export class SendEmailTool implements Tool {
};
}
this.logger.error(`Failed to send email: ${error}`);
if (isInsufficientPermissionsError(error)) {
this.logger.warn(
`Email send failed due to insufficient permissions in workspace ${context.workspaceId}`,
);
return {
success: false,
message: 'Failed to send email due to insufficient permissions',
@@ -89,6 +95,11 @@ export class SendEmailTool implements Tool {
};
}
this.logger.error(
`Failed to send email in workspace ${context.workspaceId}`,
error instanceof Error ? error.stack : undefined,
);
return {
success: false,
message: 'Failed to send email',
@@ -17,6 +17,7 @@ import { FileEmailAttachmentService } from 'src/engine/core-modules/file/file-em
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { EmailComposerService } from 'src/engine/core-modules/tool/tools/email-tool/email-composer.service';
import { EmailToolException } from 'src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception';
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
@@ -107,7 +108,21 @@ export class SendEmailResolver {
throw error;
}
this.logger.error(`Failed to send email: ${error}`);
if (error instanceof EmailToolException) {
this.logger.warn(
`Send email mutation failed with a handled email tool error (${error.code}) in workspace ${workspace.id}`,
);
return {
success: false,
error: error.message,
};
}
this.logger.error(
`Failed to send email in workspace ${workspace.id}`,
error instanceof Error ? error.stack : undefined,
);
return {
success: false,