Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 2586059ace Reply button throws unhandled error for IMAP/SMTP email accounts
https://sonarly.com/issue/14482?type=bug

Clicking "Reply" on an email thread in the side panel throws an unhandled "Account provider not supported" error for users with IMAP/SMTP connected accounts, crashing the UI.

Fix: **Two bugs fixed in `SidePanelMessageThreadPage.tsx`:**

**1. IMAP Reply crash (commit 7c8d362772):** The IMAP Driver Integration PR added `IMAP_SMTP_CALDAV` to `ALLOWED_REPLY_PROVIDERS` and added `canReply` logic for IMAP accounts with SMTP params, but left `handleReplyClick` with a `throw new Error('Account provider not supported')` placeholder for the IMAP case. Unlike Google (Gmail URL) and Microsoft (Outlook URL), IMAP has no webmail URL to deep-link to for replies.

**Fix:** Removed `IMAP_SMTP_CALDAV` from `ALLOWED_REPLY_PROVIDERS` so the Reply button is hidden for IMAP users. The switch statement keeps `IMAP_SMTP_CALDAV` as a no-op `break` to satisfy TypeScript exhaustiveness checking via `assertUnreachable`. Removed the now-unused `connectedAccountConnectionParameters` destructuring and IMAP-specific `canReply` condition.

**2. `isDefined(canReply)` regression (commit 9d57bc39e5):** The ESLint-to-OxLint migration mechanically replaced `canReply` truthiness checks with `isDefined(canReply)`. Since `canReply` is a boolean (from `useMemo`), `isDefined(false)` returns `true`, which meant: the Reply button was always rendered (line 169), never disabled (line 176), and the guard in `handleReplyClick` (line 106) never triggered.

**Fix:** Reverted `isDefined(canReply)` back to `canReply` in the three affected locations: the render condition, the disabled prop, and the click handler guard.
2026-03-13 16:24:35 +00:00
Sonarly Claude Code 4880265088 chore: additional changes for Google reCAPTCHA verification timeout during Check 2026-03-13 16:20:01 +00:00
Sonarly Claude Code a1d148694e chore: improve monitoring for Google reCAPTCHA verification timeout during Check
**`captcha.guard.ts`** — Added a `Logger` instance and a `warn`-level log when the captcha error indicates provider unreachability (`captcha-provider-unreachable` prefix). This ensures:

1. Network-level captcha failures are logged at `warn` level (not `error`) — they're transient infrastructure issues, not application bugs, so they shouldn't trigger error-level alerts
2. The error code (ETIMEDOUT, ENETUNREACH, etc.) is included in the log message for debugging
3. The existing `MetricsService.incrementCounter` call already captures the error in the `InvalidCaptcha` metric attributes, so the new log complements (not duplicates) the metric — logs give immediate visibility in pod logs, metrics give aggregate dashboard views

Previously, these failures would surface as unhandled `AggregateError` exceptions in Sentry with no application-level context. Now they're captured as structured warn logs + metric attributes, and Sentry will see a `CaptchaException` (which is a handled, expected error type) instead of a raw network error.
2026-03-13 16:19:58 +00:00
Sonarly Claude Code 24fab28025 Google reCAPTCHA verification timeout during CheckUserExists query
https://sonarly.com/issue/4574?type=bug

The twenty-server cannot reach Google's reCAPTCHA verification endpoint (`https://www.google.com/recaptcha/api/siteverify`) from its AWS eu-central-1 pod, causing the `CheckUserExists` GraphQL query to fail with an unhandled `AggregateError [ETIMEDOUT]`.

Fix: **Problem:** `GoogleRecaptchaDriver.validate()` and `TurnstileDriver.validate()` make HTTP POST requests to external captcha providers with no timeout and no error handling. When the provider is unreachable (ETIMEDOUT/ENETUNREACH), the raw `AggregateError` bubbles up through the NestJS guard chain as an unhandled 500 error.

**Fix (3 changes):**

1. **`captcha.module.ts`** — Added `timeout: 10_000` (10 seconds) to both captcha HTTP clients. This bounds the maximum wait time instead of relying on OS-level TCP timeout (~75-120s). This follows the team's existing pattern (webhook job uses `timeout: 5_000`); captcha gets a slightly higher timeout since it blocks user login.

2. **`google-recaptcha.driver.ts`** and **`turnstile.driver.ts`** — Wrapped the HTTP POST in try/catch. Network errors are caught and returned as `{ success: false, error: 'captcha-provider-unreachable: ETIMEDOUT' }` instead of throwing. This uses the existing `CaptchaValidateResult` contract, so the guard's existing failure path handles it correctly — the user sees "Invalid Captcha, please try another device" instead of a raw 500 error.

The error is NOT silently swallowed — it flows through the normal captcha failure path which increments the `InvalidCaptcha` metric counter with the error code in attributes, and then throws a `CaptchaException`.
2026-03-13 16:19:56 +00:00
@@ -44,7 +44,6 @@ const StyledButtonContainer = styled.div`
const ALLOWED_REPLY_PROVIDERS = [
ConnectedAccountProvider.GOOGLE,
ConnectedAccountProvider.MICROSOFT,
ConnectedAccountProvider.IMAP_SMTP_CALDAV,
];
export const SidePanelMessageThreadPage = () => {
@@ -62,7 +61,6 @@ export const SidePanelMessageThreadPage = () => {
messageChannelLoading,
connectedAccountProvider,
lastMessageExternalId,
connectedAccountConnectionParameters,
} = useEmailThreadInSidePanel();
useEffect(() => {
@@ -89,13 +87,10 @@ export const SidePanelMessageThreadPage = () => {
connectedAccountHandle &&
connectedAccountProvider &&
ALLOWED_REPLY_PROVIDERS.includes(connectedAccountProvider) &&
(connectedAccountProvider !== ConnectedAccountProvider.IMAP_SMTP_CALDAV ||
isDefined(connectedAccountConnectionParameters?.SMTP)) &&
isDefined(lastMessage) &&
messageThreadExternalId != null
);
}, [
connectedAccountConnectionParameters,
connectedAccountHandle,
connectedAccountProvider,
lastMessage,
@@ -103,7 +98,7 @@ export const SidePanelMessageThreadPage = () => {
]);
const handleReplyClick = () => {
if (!isDefined(canReply)) {
if (!canReply) {
return;
}
@@ -118,9 +113,8 @@ export const SidePanelMessageThreadPage = () => {
window.open(url, '_blank');
break;
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
throw new Error('Account provider not supported');
case null:
throw new Error('Account provider not provided');
break;
default:
assertUnreachable(connectedAccountProvider);
}
@@ -166,14 +160,14 @@ export const SidePanelMessageThreadPage = () => {
</>
)}
</StyledContainer>
{isDefined(canReply) && !messageChannelLoading && (
{canReply && !messageChannelLoading && (
<StyledButtonContainer>
<Button
size="small"
onClick={handleReplyClick}
title={t`Reply`}
Icon={IconArrowBackUp}
disabled={!isDefined(canReply)}
disabled={!canReply}
/>
</StyledButtonContainer>
)}