Commit Graph

6198 Commits

Author SHA1 Message Date
Vishnu Narayanan
7c16071fc7
fix: Support allowlisted private API inbox webhooks (#14548)
Self-hosted installations can now opt SafeFetch into private-network
access after SSRF hardening. The default remains unchanged: private IP
destinations are blocked unless the instance owner explicitly enables
private-network requests with `SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true`.

Fixes https://linear.app/chatwoot/issue/CW-7131
Fixes https://github.com/chatwoot/chatwoot/issues/14489
Fixes https://github.com/chatwoot/chatwoot/issues/14494

## How to use

For self-hosted installations that need API inbox webhooks, or other
SafeFetch-backed requests, to call trusted private services, enable
private-network access with a single environment variable:

```bash
SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true
```

This is disabled by default. Enable it only when the instance owner
controls the deployment network and trusts the configured URLs.
2026-05-26 17:03:19 +05:30
Sojan Jose
b981ba766f
feat: support bulk label removal (#14534)
Adds bulk label removal alongside the existing assign-label action for
conversations and contacts, so teams can clean up labels across selected
records without opening each item individually.

For conversations, the remove dropdown is scoped to labels that are
actually applied across the current selection — so agents no longer see
(or accidentally "remove") labels that aren't on any of the selected
items. For contacts, the dropdown still lists all account labels for
now; label data isn't carried on the contact list payload today, so
scoping the contact remove menu cleanly is being tracked as a follow-up.

## Closes

N/A

## How to test

- Open the conversation list, select multiple conversations, open
**Remove labels**, and confirm the dropdown only lists labels that are
applied to at least one selected conversation. Pick a label and confirm
it's removed from the selection.
- Open Contacts, select multiple contacts, use **Remove Labels**, choose
a label, and confirm the selected contacts are refreshed without that
label.
- Verify **Assign Labels** still works for conversations and contacts,
and continues to show every available label.

## What changed

- Adds an `action` prop to the shared `BulkLabelActions` dropdown so it
can render in `assign` or `remove` mode.
- Wires conversation bulk remove to the existing `labels.remove` backend
path and filters the dropdown to the union of labels applied across the
selected conversations.
- Adds contact bulk remove support through
`Contacts::BulkRemoveLabelsService`, routed by
`Contacts::BulkActionService`.
- Raises contact label save failures instead of reporting a successful
bulk action when a contact update is invalid.

## Follow-ups

- Scope the contact remove dropdown to applied labels (needs a
lightweight endpoint, or eventually `cached_label_list` on `Contact`).

## Verification

Conversation bulk remove selector:

<img width="1680" height="1050" alt="Conversation bulk remove label
selector"
src="https://github.com/user-attachments/assets/2dba4a06-c497-45e1-85b0-e700164b6b2f"
/>

Contact bulk remove selector:

<img width="1680" height="1050" alt="Contact bulk remove label selector"
src="https://github.com/user-attachments/assets/b3b89959-5978-4064-b5f9-82b1a3e571dc"
/>

Video proof:


https://github.com/user-attachments/assets/fffafe19-4e1c-4e2a-a135-c7182c06bb4d

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-05-26 15:23:51 +05:30
Aakash Bakhle
37c8e7e699
fix: firecrawl long external link (#14566)
# Pull Request Template

## Description

Fixes urls going past 255 chars, this is because of arabic urls, where
each character balloons to 8-9 characters and goes past the 255 limit

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
specs


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-05-26 14:07:07 +05:30
Muhsin Keloth
75c2f91019
chore: Enable Tiktok on paid plans Automatically (#13628)
This PR add the ability enable Tiktok integration on all paid plans.
2026-05-25 18:42:55 +05:30
Muhsin Keloth
56e30102eb
fix(whatsapp): store and surface unavailable coexistence messages (CW-7166) (#14547)
In WhatsApp coexistence setups (Business App + Cloud API on the same
number), some inbound customer messages arrive from Meta as `type:
unsupported` with error `131060` ("This message is unavailable") and no
content — typically the first message of a Click-to-WhatsApp /
Instagram-ad conversation, or a message synced from a companion device.
Chatwoot was dropping these webhooks entirely, so no contact,
conversation, or message was created. The conversation only surfaced
once an agent replied (via an `smb_message_echoes` event), starting
"headless" with zero customer context.

This change persists a placeholder message for these events so the
contact and conversation are created, and renders it with the dedicated
unsupported-message bubble that points agents to the WhatsApp app —
where the original message is still visible.
Fixes
https://linear.app/chatwoot/issue/CW-7166/whatsapp-coexistence-inbound-messages-are-silently-dropped
and https://github.com/chatwoot/chatwoot/issues/13464

<img width="3448" height="1604" alt="CleanShot 2026-05-22 at 17 49
35@2x"
src="https://github.com/user-attachments/assets/0a90ec84-9085-4cba-883d-08d9de33fa3c"
/>


## How to reproduce
1. Connect a WhatsApp Cloud (coexistence) inbox.
2. Receive an inbound message that Meta delivers as `type: unsupported`
with error `131060` (e.g. a Click-to-WhatsApp ad message, or a message
handled on a companion/primary device that fails to sync to the API).
3. **Before:** nothing is created — the conversation only appears after
an agent replies, with no record of the customer's first message.
4. **After:** the contact and conversation are created with an incoming
placeholder message rendered as the amber "unsupported" bubble: _"This
message is unsupported. You can view this message on the WhatsApp app."

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-25 18:13:59 +05:30
Vishnu Narayanan
6fbff026eb
fix: skip AutoAssignment bulk loop when no agents are online (#14500)
## Description

When an inbox has `enable_auto_assignment` and `assignment_v2` enabled
but no agents are currently online,
`AutoAssignment::AssignmentService#perform_bulk_assignment` still loaded
up to 100 unassigned conversations and iterated each one, calling
`inbox.available_agents` per conversation. Each call hits Redis presence
lookups that return empty, no conversations get assigned, and the loop
finishes having done only wasted work.

For a busy inbox with a long unassigned backlog and offline agents, this
is hundreds of Redis ops per job, multiplied by every
`AutoAssignment::AssignmentJob` enqueue from the per-save handler. The
pressure is significant when inbound volume is high.

This adds a single early-return guard: if
`inbox.available_agents.empty?`, return `0` immediately. Existing
semantics are preserved (jobs are still enqueued on conversation events;
they just exit cheaply when there is no one to assign to).

## Type of change

- [x] Performance improvement (non-breaking change)

## Test coverage

- [x] Added specs
2026-05-25 15:17:05 +05:30
Vishnu Narayanan
52da165cb7
feat: add timeout for imap email job and skip problematic emails (#11981)
# Pull Request Template

## Description

Large emails (2MB+ with multiple attachments) were causing IMAP email
processing jobs to timeout silently, blocking all subsequent emails from
being processed. This created an infinite loop where:
- Problematic emails were repeatedly fetched but never successfully
processed
- Other emails in the queue were never processed as we iterated
sequentially
  - silent failures


  ### Solution

Enhanced the FetchImapEmailsJob with individual email processing
isolation:

  ### Key Changes

1. Individual Email Processing: Changed from map to each for better
memory efficiency
2. Timeout Protection: Added configurable timeout per email (default: 60
seconds)
3. Failure Tracking: Track failed emails with 6-hour expiry for retry
opportunities
4. Skip Logic: Skip emails that have failed 3+ times to prevent infinite
loops
  5. Error Isolation: Each email is processed in its own error boundary

  ### Configuration

- Timeout: Configurable via EMAIL_PROCESSING_TIMEOUT_SECONDS using
GlobalConfigService
  - Default: 60 seconds per email
  - Failure Limit: 3 attempts before skipping
- Retry Window: 6 hours so that emails get 8 more chances in the 2 day
window

  ### Benefits

  - Prevents queue blocking: One problematic email cannot stop others
- Maintains email order: Older emails (customers waiting longer)
processed first
  - Automatic recovery: Failed emails get retry opportunities
  - Better monitoring: Clear logging when emails timeout or are skipped
- Configurable: Deployments can adjust the timeout based on their needs

This fix ensures email processing reliability while maintaining existing
functionality.

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-05-25 15:16:52 +05:30
Shivam Mishra
03fb6591e0
chore: relax conversation meta polling for high-volume accounts (#14518)
On high-volume accounts, the dashboard sidebar's conversation count
badges fall behind because a meaningful share of
`/api/v1/accounts/:id/conversations/meta` requests get rate-limited
(per-user throttle, default 30 req/min).

Root cause is in `conversationStats.js`. The tiered debounce uses
`allCount` from the last response to pick a wait interval. `allCount`
reflects the user's *current filtered scope*, not the account's true
volume — so an agent viewing a small filter on a busy account falls into
the most aggressive tier (500ms wait / 1.5s maxWait → up to 40
calls/min/tab) and trips the throttle.

## What changed

`app/javascript/dashboard/store/modules/conversationStats.js`:

- Short-tier `maxWait`: `1500 → 2000` (caps short-tier at 30/min/tab
instead of 40)
- Super-long-tier threshold: `allCount > 5000 → > 2000` (more
high-volume accounts fall into the safe 3/min/tab tier)
- Middle-tier threshold unchanged (`> 100`)

| Tier (allCount) | wait / maxWait | Calls/min/tab |
|---|---|---|
| `> 2000` | 10s / 20s | 3 |
| `> 100` | 5s / 10s | 6 |
| else | 500ms / 2s | 30 |

## Trade-off

Badge updates (including those triggered by the agent's own action) may
lag by up to the tier's `maxWait` — worst case 20s for accounts with >
2000 open conversations in the active scope. The conversation list
itself and push notifications continue to update in real time; only the
numeric badge is debounced.

## Not in scope

- Sticky-max `allCount` to fix the underlying tier-selection signal —
defer until the simpler tuning is validated in production
- Optimistic count updates on local user actions — adds non-trivial
state management for a cosmetic lag
2026-05-25 14:21:14 +05:30
Tanmay Deep Sharma
c4a6a19e9b
feat(voice): WhatsApp Cloud Calling — UI [6] (#14346)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
## Summary

Frontend for WhatsApp Cloud Calling: header / contact-panel call
buttons, ringing widget, accept/reject/hangup, mute, in-bubble audio
player + transcript, recording-on-hangup upload, mid-call reload
warning. WebRTC is browser-direct to Meta — no media server bridge.

## Closes
- https://linear.app/chatwoot/issue/PLA-150

## How to test

Requires backend support — the controller, services, model changes, and
routes ship in **#14334** (`feature/pla-150`). Merge / deploy that first
(or simultaneously); the FE alone won't function without those
endpoints.

Then on staging, for a WhatsApp Cloud + embedded-signup inbox with the
new \`Configuration → Enable voice calling\` toggle ON and webhook
registered:

1. **Outbound** — open a conversation, click the phone icon in the
conversation header (or contact panel), grant mic, your phone rings,
answer, audio both ways, hang up. Recording + transcript land in the
bubble within ~10s.
2. **Inbound** — call the business number from your phone. The
FloatingCallWidget appears bottom-right with caller name. Click accept,
audio both ways, hang up. Recording + transcript appear.
3. **Mute** — during an active WhatsApp call, click the mic icon next to
hangup. Speech stops reaching Meta until you click again.
4. **Mid-call reload guard** — try `Cmd-R` during an active call;
browser shows a confirm prompt.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-05-22 18:42:39 +05:30
Syed Muhammad Bilal
b9757447a8
fix(openapi): document webhook secret in API schema (#14199)
Fixes #13862

Updates the webhook OpenAPI schema to match the current API behavior for
webhook secrets and supported subscription events.

## Why

Current source already creates per-webhook secrets, returns `secret`
from the account webhook API, and uses that secret to sign outbound
webhook requests with `X-Chatwoot-Signature`.

The OpenAPI schema was behind that contract:
- `components.schemas.webhook` did not include the returned `secret`
field.
- Webhook subscription enums did not include the typing events that are
already available in the dashboard webhook form and handled by
`WebhookListener`.

## What this change does

- Documents `secret` on the webhook response schema.
- Documents the outbound webhook signing headers associated with
`secret`: `X-Chatwoot-Timestamp`, `X-Chatwoot-Signature`, and
`X-Chatwoot-Delivery`.
- Adds `conversation_typing_on` and `conversation_typing_off` to webhook
subscription enums.
- Regenerates the main and tag-group swagger JSON files.

## Validation

- Ran `bundle exec rails swagger:build`.
- Ran `bundle exec rspec spec/swagger/openapi_spec.rb`.
- Verified generated swagger JSON includes `secret`,
`conversation_typing_on`, and `conversation_typing_off` in the webhook
schemas.

---------

Co-authored-by: Syed Muhammad Bilal <sdmhbilal@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-22 16:10:17 +05:30
Sony Mathew
e5d66020fd
chore: made the design for unread-counts more subtle [DESN-43] (#14542)
# Pull Request Template

## Description

Made the design for unread-counts more subtle
Fixes # DESN-43

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested manually. Here are the screenshots.
Light theme:

<img width="273" height="605" alt="Screenshot 2026-05-22 at 1 28 31 PM"
src="https://github.com/user-attachments/assets/cbeccf11-41c4-4899-bbb5-f870df530260"
/>


Dark theme:
<img width="280" height="606" alt="Screenshot 2026-05-22 at 1 27 59 PM"
src="https://github.com/user-attachments/assets/3740f57d-3392-435d-9d84-75caf42df610"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-05-22 14:01:17 +05:30
eason
6b1d8203c6
fix: remove unused working hours endpoint (#13839)
Fixes #13752

Removes the standalone `working_hours` API endpoint instead of fixing
only the callback typo in `WorkingHoursController`.

## Why

The route is not used by the dashboard. The supported product flow
already updates business hours through `PATCH
/api/v1/accounts/:account_id/inboxes/:id`.

The standalone endpoint was already unusable in practice:
- The controller callback pointed to the wrong method.
- Fixing that callback alone would still leave the endpoint blocked by
missing `WorkingHourPolicy` authorization.
- Keeping the route would preserve unsupported API surface without
making the product flow better.

## What this change does

- Removes `PATCH/PUT /api/v1/accounts/:account_id/working_hours/:id`.
- Deletes `Api::V1::Accounts::WorkingHoursController`.
- Leaves the inbox working-hours update path unchanged.

Compatibility note: this removes an undocumented endpoint that was
already unusable in practice. Working-hours updates should continue to
go through the supported inbox update API.

## Validation

- Ran `bin/rails routes -g working_hours` and confirmed the standalone
working-hours API route is no longer present.
- Searched for remaining `WorkingHoursController` and `resources
:working_hours` references.

---------

Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-22 13:41:09 +05:30
Sony Mathew
4550c4130b
fix: order labels, teams, channels in sidebar by unread count (CW-7151) (#14510)
# Pull Request Template

## Description

Ordered the conversation sidebar labels, teams and channels by the
unread count.

Fixes # CW-7151

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Verified manually. Adding the screenshot below.
<img width="625" height="833" alt="Screenshot 2026-05-20 at 10 24 30 PM"
src="https://github.com/user-attachments/assets/ad04464d-0fc3-4ac7-b8cc-786e9647a299"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-05-22 13:11:29 +05:30
Sojan Jose
e09496078b
fix(widget): improve dark mode select options (#14538)
Fixes the web widget select dropdown styling in dark mode so native
select options remain readable against the widget's dark UI.

Closes
None

## Screenshots

Previous state:

<img width="426" height="764" alt="Previous dark mode dropdown issue"
src="https://github.com/user-attachments/assets/812fa88c-ae5a-4769-be14-748fbbaf7dfe"
/>

Current state:

<img width="1210" height="610" alt="Current dark mode dropdown styling"
src="https://github.com/user-attachments/assets/0ec9b6d7-025d-4b97-b43e-ef026857f9c4"
/>

## How to test

1. Enable the web widget pre-chat form with a list/select field.
2. Load the widget with dark mode enabled.
3. Open the select field and verify the select control and option text
remain readable in dark mode.

## What changed

- Adds light/dark color-scheme handling for widget native selects.
- Applies explicit option background and text colors so dark-mode select
options do not inherit unreadable colors from the browser popup.

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-05-22 12:59:48 +05:30
Khoa Nguyen
4cdfe4168c
chore: resolve sass and vue compiler deprecation warnings (#13794) 2026-05-22 12:16:43 +05:30
Sivin Varghese
0722750a55
chore: Captain reply actions not showing correctly with content (#14160) 2026-05-22 12:16:19 +05:30
Aakash Bakhle
bef25781de
feat(attachments): add XML and PFX file support (#14539)
Update frontend allowed file types and FileIcon mapping, and backend
Attachment constants to accept .xml and .pfx files

# Pull Request Template

## Description
Customer also wanted XML support along with .pfx

Following up on #14456

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
locally

<img width="864" height="512" alt="CleanShot 2026-05-22 at 11 43 20@2x"
src="https://github.com/user-attachments/assets/4cbf65d4-b919-4a4b-bf75-a4f2e8690586"
/>

<img width="870" height="1440" alt="CleanShot 2026-05-22 at 11 44 03@2x"
src="https://github.com/user-attachments/assets/e763b49d-4365-4c45-9b43-b0c39af87656"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-05-22 11:55:16 +05:30
Sojan Jose
1d7a9093d2
fix: clarify agent availability swagger fields (#14533)
Clarifies the agent availability API documentation so request payloads
use the writable `availability` field, while `availability_status`
remains documented as a read-only response field.

## Closes

Closes #13873

## Why

The backend already supports updating an agent's configured availability
through `availability`, but the Swagger request payloads documented
`availability_status`. That made clients follow a read-only response
field and see successful requests without the intended availability
change.

## What changed

- Replaces `availability_status` with `availability` in agent
create/update request schemas.
- Updates the availability enum to `online`, `busy`, and `offline`.
- Marks response `availability_status` as read-only and explains that it
is derived from configured availability, auto-offline, and presence.
- Regenerates the combined and tag-group Swagger JSON files.

## Validation

- `bundle exec rails swagger:build`
- `bundle exec rspec spec/swagger/openapi_spec.rb`
- `git diff --check`
2026-05-22 11:33:19 +05:30
Sivin Varghese
3c67c41544
chore: support PFX filetype in attachment uploads (#14456)
# Pull Request Template

## Description

This PR expands the default upload rules to support PFX certificate
files (`application/x-pkcs12`, `application/pkcs12`, `.pfx`) across
private notes, Website, Email, and Telegram channels.

Also adds `.xls` / `.xlsx` extension fallbacks for cases where browsers
upload Excel files with an empty or generic MIME type.

### Utils Repo PR: https://github.com/chatwoot/utils/pull/61

Fixes
https://linear.app/chatwoot/issue/CW-7085/support-more-file-types-in-private-notes-and-in-app

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screenshots
<img width="330" height="218" alt="image"
src="https://github.com/user-attachments/assets/80823250-893e-4509-adb9-61f845359151"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: aakashb95 <aakashbakhle@gmail.com>
2026-05-22 11:09:27 +05:30
Muhsin Keloth
d0ecdc14d8
feat(webhooks): Emit inbox_updated when an inbox is disconnected (#14504)
Chatwoot now lets external apps know when an inbox loses its connection
and needs re-authentication. When a channel's authorization expires (for
example, an email inbox disconnects), Chatwoot fires an `inbox_updated`
webhook reflecting the new `reauthorization_required` status, and fires
it again once the inbox is re-authenticated. Integrators can keep their
own view of which inboxes are healthy without polling the API.

This is gated behind the `ENABLE_INBOX_EVENTS` installation flag — the
**Inbox updated** webhook subscription only appears in the dashboard
when that flag is enabled, so no event is offered that the backend
wouldn't dispatch.

Fixes
https://linear.app/chatwoot/issue/CW-7148/emit-inbox-webhook-when-an-inbox-is-disconnected

## How to test

1. Set `ENABLE_INBOX_EVENTS=true` and restart the app.
2. In **Settings → Integrations → Webhooks**, add a webhook and
subscribe to **Inbox updated**.
3. Disconnect an inbox — let an email/Instagram channel hit its
auth-error threshold, or run `inbox.channel.prompt_reauthorization!` in
a console.
4. The endpoint receives an `inbox_updated` event whose
`changed_attributes` shows `reauthorization_required` flipping to
`true`.
5. Re-authenticate the inbox (or run `inbox.channel.reauthorized!`) —
the endpoint receives the `true → false` transition.
6. Confirm the **Inbox updated** option is hidden when
`ENABLE_INBOX_EVENTS` is unset.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-22 09:00:18 +04:00
Tanmay Deep Sharma
b1db6c3e9b
fix: make zadd function optimised to stay in rubocop limits (#14520)
## Description
Fixes rubocop for alfred.rb file on develop 

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-05-21 17:24:51 +05:30
Shivam Mishra
3d20a7b049
feat: generate Help Center for Onboarding (#14370)
## Manually triggering help center generation

Open a Rails console (`bundle exec rails console`):

```ruby
account = Account.find(<ACCOUNT_ID>)
user    = account.users.first

# Optional: refresh brand info from the customer's website
domain = 'example.com'
result = WebsiteBrandingService.new("noreply@#{domain}").perform
account.update!(
  name: result[:title].presence || account.name,
  custom_attributes: account.custom_attributes.merge('website' => domain, 'brand_info' => result)
)

# Optional: wipe existing portals so a fresh one is created
account.portals.destroy_all

Onboarding::HelpCenterCreationService.new(account, user).perform
```

Sidekiq must be running — articles are written by
`Onboarding::HelpCenterArticleGenerationJob`. Avoid running on
production; generation calls the LLM provider.


### Generation flow (Happy Path) 

```mermaid
sequenceDiagram
    autonumber

    participant Kickoff as HelpCenterCreationService
    participant DB as DB
    participant GenJob as HelpCenterArticleGenerationJob
    participant Curator as HelpCenterCurator
    participant Firecrawl as Firecrawl
    participant CuratorLLM as Curation LLM
    participant Redis as Redis Progress
    participant WriterJob as HelpCenterArticleWriterJob
    participant Builder as HelpCenterArticleBuilder
    participant WriterLLM as Writer LLM
    participant Cable as ActionCable

    Kickoff->>DB: Create portal for account<br/>homepage_link=https://chatwoot.com
    Kickoff->>DB: Attach brand logo if available
    Kickoff->>GenJob: Enqueue generation job<br/>account_id, portal_id, user_id, generation_id

    GenJob->>Curator: Curate help center plan
    Curator->>Firecrawl: map https://chatwoot.com<br/>search: docs help support faq
    Firecrawl-->>Curator: Return discovered links
    Curator->>CuratorLLM: Select categories + article plans<br/>from discovered links only
    CuratorLLM-->>Curator: Return categories, articles, allowed_urls

    GenJob->>DB: Create portal categories
    GenJob->>GenJob: Stamp articles with category_id
    GenJob->>GenJob: Filter article URLs against allowed_urls
    GenJob->>GenJob: Drop articles with no category<br/>or no approved source URLs

    GenJob->>Redis: Start progress<br/>status=generating, total=N, finished=0

    loop For each approved article
      GenJob->>WriterJob: Enqueue writer job<br/>title, category_id, approved URLs
    end

    par Writer jobs run independently
      WriterJob->>Builder: Build article from approved URLs
      Builder->>Firecrawl: batch_scrape approved URLs
      Firecrawl-->>Builder: Return Markdown source pages
      Builder->>WriterLLM: Rewrite sources into one article
      WriterLLM-->>Builder: Return title, description, Markdown content
      Builder->>DB: Create draft portal article<br/>meta.source_urls
      WriterJob->>Redis: Increment finished count
      WriterJob->>Cable: Broadcast help_center.article_generated
    end

    WriterJob->>Redis: If finished >= total<br/>mark status=completed
    WriterJob->>Cable: Broadcast help_center.generation_completed
```

### Redis State Management

```mermaid
 stateDiagram-v2
    [*] --> active_pointer_set
    active_pointer_set --> generating: generation job creates valid plan
    active_pointer_set --> skipped: curation skipped/failed

    generating --> generating: each writer job increments finished
    generating --> completed: finished == total
    generating --> ignored_completion: generation_id superseded

    skipped --> [*]
    completed --> [*]
    ignored_completion --> [*]
```
2026-05-21 16:25:01 +05:30
Tanmay Deep Sharma
3cd8cf43ce
fix: atomically claim conversation to prevent duplicate assignment (#14495)
## Description

Fixes a bug under Assignment V2 where a single conversation could be
reassigned dozens of times in a row by the system, producing long stacks
of "Assigned to X by Automation System via <policy>" activity messages
alternating between agents. After this change each unassigned
conversation is assigned exactly once, even on busy inboxes.

## Fixes # (issue)


## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

## How to reproduce
1. Enable `assignment_v2` on an account with at least 2 online agents in
an inbox.
2. Generate sustained resolve/snooze activity in the inbox (each one
enqueues `AutoAssignment::AssignmentJob` for the whole inbox).
3. Watch any one unassigned conversation while the jobs drain — pre-fix
it picks up multiple back-to-back "Assigned to …" activity rows
alternating between agents.


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-05-21 16:14:28 +05:30
Sony Mathew
f33e469e9a
feat: Unread Count: Frontend changes for showing unread count badges (3/3)[CW-6851] (#14372)
# Pull Request Template

## Description
This is the third and final PR in a series of PRs for Introducing unread
counts in the sidebar for inboxes and labels.

In this PR:
* Added frontend changes to show the badges for unread counts for
Inboxes and Labels
* Added specs for the changes

Issue:
https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts



## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested this locally. Cases to test:
* Send a message from the widget and see if the count changes
* Mark a conversation as unread and see the count change for inbox
* Open an unread conversation as agent and see the count go down
* Add a label to an unread conversation from sidebar right click action
without opening the conversation and see the count of un-reads on the
label change

Added the screenshot of how it will look like

<img width="614" height="990" alt="Screenshot 2026-05-05 at 7 00 11 PM"
src="https://github.com/user-attachments/assets/99fbaa9f-bcf2-4d8d-86e2-5727f652a9dd"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-20 19:21:25 +05:30
Sony Mathew
27f2c2b392
feat: Unread Count: added api, store refresher, invalidation and events (2/3)[CW-6851] (#14369)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
# Pull Request Template

## Description

This is the second PR in a series of PRs for Introducing unread counts
in the sidebar for inboxes and labels.

In this PR:

* added api for unread counts 
* Added the store refresher and invalidation with event listeners
* Added action cable event
* Added specs for the changes

Issue:
https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-20 17:36:09 +05:30
Muhsin Keloth
40deaef458
feat: Store WhatsApp BSUID identifiers from inbound webhooks (#14436)
Adds storage support for WhatsApp business-scoped user identifiers
received from Meta Cloud API and Twilio WhatsApp webhooks. The change
keeps existing phone-based behavior intact, stores BSUID and parent
BSUID values as additional `contact_inboxes.source_id` rows for the same
contact, and allows BSUID-only inbound messages to create contacts,
conversations, and messages without requiring a phone number.

Related: https://github.com/chatwoot/chatwoot/issues/13837

**What changed**
- Extended WhatsApp source ID validation to accept regular BSUID and
parent BSUID formats.
- For Meta Cloud API, stores phone, `user_id`, and `parent_user_id`
identifiers as contact inbox source IDs when they are present.
- For Twilio WhatsApp, stores phone, `ExternalUserId`, and
`ParentExternalUserId` identifiers as contact inbox source IDs while
preserving the existing `whatsapp:` Twilio source ID shape.
- Supports BSUID-only inbound messages by creating a contact, contact
inbox, conversation, and message even when the phone number is missing.
- Links phone-first and later BSUID-only messages to the same contact
when the first payload contains both phone and BSUID.
- Stores WhatsApp usernames in contact `additional_attributes`, matching
existing social channel patterns.
- Keeps existing phone-based outbound and new-conversation behavior
unchanged for this milestone.

**How to test**
1. Send a Meta Cloud webhook payload with both `wa_id` and `user_id`.
2. Verify Chatwoot creates or finds the phone `contact_inbox` and also
creates a BSUID `contact_inbox` for the same contact.
3. Send a later Meta Cloud payload for the same user with only `user_id`
/ `from_user_id`.
4. Verify Chatwoot finds the BSUID `contact_inbox` and creates the
inbound message without requiring a phone number.
5. Send a Twilio WhatsApp webhook with `From: whatsapp:+E164`,
`ExternalUserId`, and optionally `ParentExternalUserId`.
6. Verify Chatwoot stores the Twilio phone and BSUID identifiers as
`whatsapp:`-prefixed source IDs for the same contact.
7. Send a Twilio WhatsApp webhook where `From` is `whatsapp:<BSUID>` and
there is no phone number.
8. Verify Chatwoot creates the contact, contact inbox, conversation, and
message without a phone number.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-20 13:36:43 +04:00
Sony Mathew
3fae800936
feat: base layer for unread counts (store, counter and builder) (1/3)[CW-6851] (#14368)
## Description

This is the first PR in a series of PRs for Introducing unread counts in
the sidebar for inboxes and labels.

In this PR:

* Added the unread store, counter and builder modules
* Added redis keys for unread count management
* Added specs for all 3 modules, some specs are for testing enterprise
only feature like specific roles and permissions which are added in the
respective enterprise folder itself.

**Note**
None of this changes affect anything else and nothing is wired to
existing modules.

Issue:
https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-20 14:26:21 +05:30
Sony Mathew
1913ccadfa
fix: [CW-7141] fix gem audit issue for sidekiq-cron and devise (#14497)
# Pull Request Template

## Description
* sidekiq-cron upgraded to 2.4.0
* Sidekiq constrained to stay on 7.3.x
* Devise advisory ignored in .bundler-audit.yml with the reason:
Chatwoot does not enable Timeoutable, so the timeout redirect path is
not reachable


### Details
The sidekiq-cron upgrade is from 1.12.0 to 2.4.0.

What changed that matters for us:

Fixes the reported Sidekiq Web UI reflected XSS in 2.4.0.
Adds namespace handling changes from the 2.x series. Chatwoot does not
use custom cron namespaces in config/schedule.yml, so the migration
guide says no action is needed for our usage.
Drops support for old Sidekiq/Redis versions. We are still on Sidekiq
7.3.1, which is supported.
Adds new dependencies: cronex and unicode.
Keeps the same APIs we use: Sidekiq::Cron::Job.load_from_hash!(schedule,
source: 'schedule'), Sidekiq::Cron::Job.destroy(name), and require
'sidekiq/cron/web' still exist.
Chance of breakage: low, based on the current Chatwoot usage.

The main thing I would watch after deploy is scheduled job registration
in Sidekiq. The one subtle area is namespace behavior: if production has
custom, manually-created cron jobs using non-default namespaces,
load_from_hash! cleanup behavior could matter. For the committed
config/schedule.yml jobs, which do not specify namespaces, they should
continue in the default namespace.

For concerns around Devise, it does not look exploitable in current
Chatwoot, because Chatwoot does not enable Devise :timeoutable.
I checked:
app/models/user.rb (line 59) lists the Devise modules, and :timeoutable
is not included.
config/initializers/devise.rb (line 164) has the timeoutable section,
but config.timeout_in is commented out.
SuperAdmin inherits from User, so it does not add a separate timeoutable
path either.
So from a practical security perspective: the vulnerable redirect path
requires warden_message == :timeout, which is only produced by Devise
Timeoutable. Since Chatwoot does not use Timeoutable, this warning is
not currently reachable.
Is the patch really needed? Strictly for current exploitability: no.

Fixes #CW-7141

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Spec and lints and change-log checks with codex.


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-05-19 20:39:40 +05:30
Sivin Varghese
bca95efb82
feat: add image resize support in articles (#14293) 2026-05-19 19:34:43 +05:30
Pranav
6560dbb68d
feat: Add an option on the dashboard to allow switching help center layout (#14491)
<img width="633" height="431" alt="Screenshot 2026-05-18 at 12 32 55 PM"
src="https://github.com/user-attachments/assets/682d4c5f-4c76-465b-8d2f-92fbc2bb2a40"
/>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-05-19 06:42:48 -07:00
Sivin Varghese
497d34c097
fix: render markdown in CSAT survey messages (#14468) 2026-05-19 10:27:14 +05:30
chinatsu1124
4371793741
fix: captain-v2 cannot see image attachments shared via email (#14449)
## Description

Inbound email attachments are stored with `file_type: 'file'` regardless
of their actual MIME type. As a result, image screenshots shared by
customers via email are not exposed to Captain V2's multimodal pipeline
— `Captain::OpenAiMessageBuilderService#attachment_parts` selects images
via `attachments.where(file_type: :image)` and emits a placeholder
`"User has shared an attachment"` text part instead of an `image_url`
part. The model never gets the image, so Captain keeps asking the
customer to retype information that is already visible in the
screenshot.

This PR makes the email mailbox derive `file_type` from the blob's
`content_type` using the existing shared `FileTypeHelper`, matching how
every other inbound channel (`twilio`, `sms`, `telegram`, `line`,
`tiktok`, `twitter`, `messenger`) and `MessageBuilder` already classify
attachments.

Fixes #14448

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Reproduced and verified on a self-hosted production instance:

1. Real customer reply via email with a PNG screenshot of an in-app
error.

   Before:
   ```ruby
   a = Message.find(<id>).attachments.first
   a.file_type                # => "file"
   a.file.blob.content_type   # => "image/png"
Captain::OpenAiMessageBuilderService.new(message:
a.message).generate_content
   # => [{type: 'text', text: '...'},
# {type: 'text', text: 'User has shared an attachment'}]  no image_url
   ```
Captain reply: "Please copy and paste the full error text…" (model never
saw the image).

2. After the patch + force-recreate, same conversation:
   ```ruby
   a.file_type                # => "image"
Captain::OpenAiMessageBuilderService.new(message:
a.message).generate_content
   # => [{type: 'text',      text: '...'},
# {type: 'image_url', image_url: {url: 'https://.../<blob>.png'}}] 
   ```
Captain reply now correctly references the on-screen error text from the
screenshot via the multimodal vision path — no more deflection.

3. Regression sanity-check on non-image attachments (PDF / Office docs):
`file_type` falls through to `:file`, behavior unchanged.

## Notes for self-hosted operators

Existing email image attachments in the DB will still have `file_type:
'file'`. A one-shot backfill is straightforward and safe (no data loss,
only metadata):

```ruby
Attachment.joins(message: :conversation)
          .where(messages: { content_type: 'incoming_email' })
          .where(file_type: 'file')
          .find_each do |a|
  next unless a.file.attached?
  ct = a.file.blob.content_type.to_s
  next unless ct.start_with?('image/', 'audio/', 'video/')
  new_type = ct.start_with?('image/') ? :image : (ct.start_with?('video/') ? :video : :audio)
  a.update_columns(file_type: Attachment.file_types[new_type])
end
```

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — happy to add a
`mailbox_helper_spec` example for `process_regular_attachments` if
maintainers prefer; existing specs in that file focus on inline-image
handling.

---------

Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-05-19 10:11:32 +05:30
Pranav
64585faff0
feat: Add a documentation layout design for public help center portal (#14403)
https://github.com/user-attachments/assets/fc4d15f9-2b54-4627-940f-94772ec739b1

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-05-18 12:30:08 -07:00
Sony Mathew
e05008e0a4 Merge branch 'release/4.14.0' into develop 2026-05-18 21:55:01 +05:30
Sony Mathew
92f8c13ce5 Bump version to 4.14.0 2026-05-18 21:53:43 +05:30
Muhsin Keloth
c4089f2226
fix(csat): Require confirmation before submitting rating (#14450)
Customers reported that the CSAT survey was recording their rating the
instant they tapped a star — leaving no chance to correct an accidental
pick. This change lets the customer freely change their selection until
they actually submit the comment/feedback. The rating still saves on
click (so we don't lose ratings when a customer never types a comment),
but it stays editable until the comment form is submitted. Once that
happens, the rating locks.

The flow on both surfaces:

- Customer taps a star/emoji → rating is saved.
- Customer taps a different star/emoji → previous save is overwritten
with the new value.
- Customer types a comment and submits → latest rating + comment are
saved together.
- After that submit, the rating is locked and can't be changed.

Two surfaces are updated:

- **Standalone survey page** (`/survey/responses/:uuid`) — the rating
buttons remain re-tappable until the Feedback form is submitted; once
submitted, both rating and feedback lock.
- **In-conversation widget CSAT** — same behavior, the inline
arrow-submit button on the feedback form is the locking action.

In-flight guards prevent a race where the customer changes their pick
mid-network-call (raised by the codex review on the earlier revision):
while a save is in flight, the rating controls are temporarily disabled
so the request and the displayed selection can't diverge.

## Closes

-
https://linear.app/chatwoot/issue/CW-7061/csat-star-rating-submits-on-first-click-needs-confirmation-step

## How to test

**Standalone survey page**
1. Enable CSAT on any inbox (Settings → Inboxes → Configuration → CSAT
survey).
2. Resolve a conversation in that inbox so a CSAT message is generated.
3. Open the survey URL:
`{FRONTEND_URL}/survey/responses/{conversation.uuid}` (easiest: `bundle
exec rails runner 'puts Conversation.joins(:messages).where(messages: {
content_type: "input_csat" }).last.csat_survey_link'`).
4. Tap a star/emoji — confirm the rating saves (Network panel shows a
PUT to `/public/api/v1/csat_survey/{uuid}`).
5. Tap a different star/emoji — confirm a second PUT goes out with the
new value; the latest selection is reflected.
6. Type a comment and hit Submit feedback — confirm rating + feedback
persist; both controls now lock.
7. Reload the page — the locked state is rehydrated correctly.

**Widget CSAT**
1. From an inbox with CSAT enabled, resolve a conversation that has an
active widget session.
2. In the widget, the CSAT card appears with stars/emojis + the inline
comment box.
3. Tap a star/emoji — confirm a PATCH goes out and the selection visibly
updates.
4. Tap different stars/emojis — confirm each overrides the previous
save.
5. Type a comment and click the arrow — rating + comment submit
together; stars lock.

Both display types (emoji and 5-star) should behave consistently.

## What changed

- `app/javascript/survey/views/Response.vue` — `selectRating()` saves on
every tap and short-circuits while a save is in flight (or after
feedback was submitted). Rating components are disabled by
`isFeedbackSubmitted || isUpdating` so the lock follows the feedback
submission, not the first rating tap.
- `app/javascript/survey/components/Rating.vue` — new `isDisabled` prop.
The disabled / hover styling and click guard key off it instead of the
presence of `selectedRating`, so emojis stay re-clickable until the
feedback step locks them.
- `app/javascript/shared/components/CustomerSatisfaction.vue` — same
shape for the widget: rating click auto-submits and re-clicks override
the previous save; controls disabled while a submit is in flight;
emoji-button styling and the inline `StarRating` lock on
`isFeedbackSubmitted || isUpdating`.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-05-18 21:46:30 +05:30
Sivin Varghese
bcb66cdcc0
chore: Support creating articles from category view (#14406)
# Pull Request Template

## Description

This PR adds support for creating articles directly from the category
view. Previously, articles could only be created from the main articles
page. With this change, users can now create a new article while
browsing a specific category, making the workflow faster and more
convenient.

Fixes
https://linear.app/chatwoot/issue/CW-7050/create-an-article-when-inside-a-category

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screencast


https://github.com/user-attachments/assets/e5a72a85-676e-4747-948a-6b1a19d2089f




## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-05-18 18:09:53 +05:30
Captain
7f0d5caca4
chore: Update translations (#14276)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-18 17:31:05 +05:30
Sojan Jose
1d2f3e86dd
feat(companies): track company last activity (#14435)
Tracks company recency from linked contact activity so the Companies
list and detail page can show/sort by real customer engagement instead
of generic record updates.

## Closes

None.

## Why

Company recency should reflect activity from people associated with the
company. This keeps the signal tied to persisted contact activity,
without treating passive online presence or widget heartbeat pings as
company activity.

## What Changed

- Adds a company helper to record `last_activity_at` from linked contact
activity.
- Rolls up `Contact#last_activity_at` changes to the associated company.
- Initializes company activity when an already-active contact is
associated with a company, including the business-email auto-association
path.
- Throttles company activity rollups to once every 5 minutes per company
to avoid unnecessary writes during active conversations.
- Treats company activity as monotonic: unlinking, moving, or deleting
contacts does not move a company's activity timestamp backwards.
- Leaves historical backfill, online presence tracking, widget visit
tracking, and richer activity attribution out of scope.

## How to Test

1. Open an account with Companies enabled and visit the Companies list.
2. Trigger activity for a contact that belongs to a company, for example
by receiving or sending a message in that contact's conversation.
3. Confirm the linked company shows a recent activity timestamp in the
Companies list/detail page after the contact activity updates.
4. Associate an already-active contact with a company and confirm the
company receives that contact's existing activity timestamp.
5. Confirm repeated contact activity within a short window does not
continuously rewrite the company timestamp.

---------

Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-05-18 15:01:05 +05:30
Aakash Bakhle
3253e863ed
fix: validate OpenAI hook credentials (#14068)
# Pull Request Template

## Description

- Validates openai key while configuring hooks
- added backfill logic

Fixes # (issue)

## Type of change

- [x] New feature (non-breaking change which adds functionality)


## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
locally

<img width="1710" height="1234" alt="CleanShot 2026-04-15 at 16 15
02@2x"
src="https://github.com/user-attachments/assets/3d319fe0-19f9-4fd0-9308-74987daac2e1"
/>

<img width="2884" height="1136" alt="CleanShot 2026-05-11 at 19 22
53@2x"
src="https://github.com/user-attachments/assets/5eae8650-985b-4c4a-af42-35f7175ff52d"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-05-18 14:08:57 +05:30
Aakash Bakhle
059d840272
feat: Refresh llm settings when superadmin configs change [AI-151] (#14388)
# Pull Request Template

## Description

fixes:
https://linear.app/chatwoot/issue/AI-151/captains-super-admin-config-dont-get-applied-into-rails-without

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)


## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
specs and locally

To test locally: 
go to super admin -> settings -> captain -> Change endpoint to something
incorrect
go to local app -> captain -> playground -> try chatting (should fail
due to incorrect endpoint)

now in super admin captain settings, set the correct endpoint then chat
in playground. Now it should work.

Current develop code doesn't reflect the changes in installation config
for captain instantly, needs a server restart.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-05-18 14:08:26 +05:30
Shivam Mishra
b8deb89613
fix: make SAML callback session independent (#14467)
This PR makes SAML login independent of Rails session cookies

## Problem

The normal SAML login flow should be straightforward:

- User opens Chatwoot.
- Chatwoot creates `_chatwoot_session`.
- User starts SSO.
- Chatwoot redirects the browser to the SAML provider.
- The provider authenticates the user.
- The provider sends the browser back to Chatwoot's ACS URL.
- Chatwoot reads the SAML response, finds or creates the user, and logs
them in.

The fragile step is the ACS callback. Most SSO flows return to the app
through browser redirects where cookies usually pass through as
expected. **ADFS commonly returns the SAML response with a cross-site
POST**. With Chatwoot's session cookie using `SameSite=Lax`, browsers
may not send `_chatwoot_session` on that POST.

SAML validation itself does not need the old Rails session cookie. The
problem was our callback handoff after validation. DeviseTokenAuth
stores the verified OmniAuth payload in Rails session, then redirects to
a second callback route. If the browser does not preserve that session,
Chatwoot has already received a valid SAML response but can no longer
finish login.

## Solution

This PR removes the session-backed handoff for SAML only:

- The SAML callback completes login in the same request where OmniAuth
validates the SAML response.
- Chatwoot reads the verified auth payload directly from
`request.env['omniauth.auth']`.
- Account context and RelayState come from callback params or OmniAuth
env data, not Rails session.
- Other OmniAuth providers continue using the existing DeviseTokenAuth
flow.
- Mobile SAML still works when the IdP returns `RelayState=mobile`; the
callback redirects to the mobile deep link with the generated SSO token.

The previous SAML override used `303 See Other` to avoid replaying the
SAML POST into the second callback route. This change keeps that intent,
but removes the second callback route for SAML entirely.

## Screen recording

### SP Initiated

https://github.com/user-attachments/assets/b0735e93-3864-4cc3-b6fc-419fff4b549e

### IDP Initiated

https://github.com/user-attachments/assets/3ded0246-933c-4c85-9b7c-fa15fdc34883

## Testing

Manual validation:

- Complete a SAML login.
- In the browser network trace, find the IdP POST to
`/omniauth/saml/callback?account_id=<account-id>`.
- Confirm it redirects directly to `/app/login?...sso_auth_token=...`
for web login.
- For mobile, confirm `RelayState=mobile` redirects to the configured
mobile deep link.
- Confirm there is no intermediate `/auth/saml/callback` request.

Testing with mocksaml.com:

- Configure Chatwoot with a public `FRONTEND_URL`.
- Set the mocksaml ACS URL to:

```text
https://<chatwoot-host>/omniauth/saml/callback?account_id=<account-id>
```

- Set the mocksaml audience/SP entity ID to the value shown in Chatwoot
SAML settings, usually:

```text
https://<chatwoot-host>/saml/sp/<account-id>
```

- Use an email returned by mocksaml that exists in the SAML-enabled
account.
- Start login from Chatwoot's SSO login page.
- Confirm the callback redirects directly to the app login URL with an
SSO token.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-18 12:52:45 +05:30
Shivam Mishra
ef27e571f7
feat: enable quoted reply for everyone (#14469)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Quoted email replies is now available to every account by default.
Previously this was gated behind the `quoted_email_reply` account-level
feature flag, so accounts needed it toggled on (via Super Admin) before
agents saw the toggle in the reply box.

## How to test

1. Open any conversation on an email inbox.
2. Confirm the **Quote previous email** toggle is visible in the reply
box (and is **not** visible on private notes or non-email channels).
3. Toggle it on, type a reply, and send — the outbound email should
include the quoted prior email below your message.
4. Toggle it off and send another reply — the quoted block should not
appear.
5. The toggle preference should persist per channel type (UI setting),
as before.
6. Verify the toggle works on a brand new account with no feature flags
flipped on (previously it would have been hidden).

## What changed

- Removed all `isFeatureEnabledonAccount(..., QUOTED_EMAIL_REPLY)` gates
from `ReplyBox.vue`, so the toggle and quoted-content behavior are
unconditional on email channels.
- Removed the `QUOTED_EMAIL_REPLY` constant from
`dashboard/featureFlags.js`.
- Marked the flag as `deprecated: true` in `config/features.yml` (kept
the entry in place to preserve FlagShihTzu bit positions on existing
accounts; `deprecated: true` hides it from the Super Admin UI).
- Dropped the now-unnecessary
`account.enable_features('quoted_email_reply')` setup from the message
builder spec.
2026-05-15 10:59:48 -07:00
Baptiste Fontaine
1528fcde0c
fix: missing widget es translations (#14375)
# Pull Request Template

## Description

There were English strings in the Spanish i18n file for the widget. This
PR translates them.

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-15 12:16:49 +05:30
Renato Ascencio
5f6bd951b9
fix: portals#create returns 500 when custom_domain is omitted (#14400)
## Description

`POST /api/v1/accounts/:account_id/portals` returns a generic 500
(`{"status":500,"error":"Internal Server Error"}`) whenever the request
body omits `custom_domain`. Root cause: `parsed_custom_domain` calls
`URI.parse(@portal.custom_domain)` and `URI.parse(nil)` raises
`URI::InvalidURIError`. Existing callers either had to know to pass
`"custom_domain": ""` as a workaround or hit a 500 with no useful
diagnostic.

This PR guards `parsed_custom_domain` against blank values so the
existing fall-through (`else @portal.custom_domain`) applies —
equivalent to passing an empty string.

It also moves the `process_attached_logo` guard from the helper into the
`create` call site so `create` mirrors `update` (`process_attached_logo
if params[:blob_id].present?`) and avoids an unnecessary signed-blob
lookup on every create that doesn't include a logo.

Fixes #14397

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Two new request specs in
`spec/controllers/api/v1/accounts/portals_controller_spec.rb` covering
the regression:

- `creates portal when custom_domain is omitted from request body` — the
previously-broken case, now returns 200.
- `creates portal when custom_domain is blank` — verifies the existing
workaround (`"custom_domain": ""`) still works after the change.

Manually verified against `chatwoot/chatwoot:latest` Docker image before
the fix (500) and against this branch (200) using the curl repro from
the issue.

```bash
curl -X POST "https://<host>/api/v1/accounts/<account_id>/portals" \
  -H "Content-Type: application/json" \
  -H "api_access_token: <token>" \
  -d '{"name":"Test Portal","slug":"test-portal","color":"#3b82f6"}'
```

Before: `{"status":500,"error":"Internal Server Error"}`
After: `200 OK` with the portal payload.

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation (no doc
change needed — controller behaviour, fully backward-compatible)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-15 11:11:18 +05:30
dependabot[bot]
cfc7699b7e
chore(deps): bump net-imap from 0.4.20 to 0.4.24 (#14361)
Bumps [net-imap](https://github.com/ruby/net-imap) from 0.4.20 to
0.4.24.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/net-imap/releases">net-imap's
releases</a>.</em></p>
<blockquote>
<h2>v0.4.24</h2>
<blockquote>
<p>[!IMPORTANT]
<em>The <code>0.4.x</code> release branch will only receive critical
security fixes, and will be unsupported when ruby 3.3 is EOL.
Please upgrade to a newer version.</em></p>
</blockquote>
<h2>What's Changed</h2>
<h3>🔒 Security</h3>
<p>This release contains fixes for <strong>multiple
vulnerabilities</strong> concerning <em><strong><code>STARTTLS</code>
stripping</strong></em>, argument validation, and denial of service
attacks.</p>
<blockquote>
<p>[!WARNING]
<a
href="https://redirect.github.com/ruby/net-imap/pull/666">ruby/net-imap#666</a>
fixes a <code>STARTTLS</code> stripping vulnerability
(GHSA-vcgp-9326-pqcp).
Without this fix, a man-in-the-middle attacker can cause
<code>Net::IMAP#starttls</code> to return &quot;successfully&quot;,
<strong><em>without starting TLS</em></strong>.</p>
</blockquote>
<blockquote>
<p>[!IMPORTANT]
Argument validation is significantly improved. Several injection
vulnerabilities have been fixed:
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>
fixes CRLF/command/argument injection via Symbol arguments
(GHSA-75xq-5h9v-w6px).
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>
fixes CRLF/command/argument injection via the <code>attr</code> argument
to <code>#store</code>/<code>#uid_store</code> (GHSA-hm49-wcqc-g2xg)
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>
fixes CRLF/command/argument injection via the <code>storage_limit</code>
argument to <code>#setquota</code> (GHSA-hm49-wcqc-g2xg).
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>
fixes CRLF/command injection via <code>RawData</code>
(GHSA-hm49-wcqc-g2xg):</p>
<ul>
<li><code>#search</code> and <code>#uid_search</code> send
<code>criteria</code> as raw data, when it is a String</li>
<li><code>#fetch</code> and <code>#uid_fetch</code> send
<code>attr</code> as raw data, when it is a String.
When <code>attr</code> is an Array, its String members are sent as raw
data.</li>
</ul>
</blockquote>
<blockquote>
<p>[!CAUTION]
<code>RawData</code> does not defend against <em>other</em> forms of
argument injection! It is an intentionally low-level API.</p>
</blockquote>
<blockquote>
<p>[!NOTE]
Two denial of service vulnerabilities have been addressed.
These are generally only relevant when connecting to an <em>untrusted
hostile server</em> (or without TLS).</p>
<p><a
href="https://redirect.github.com/ruby/net-imap/pull/651">ruby/net-imap#651</a>
fixes quadratic time complexity when reading large responses containing
many string literals (GHSA-q2mw-fvj9-vvcw).
<a
href="https://redirect.github.com/ruby/net-imap/pull/655">ruby/net-imap#655</a>
adds a configurable <code>max_iterations</code> count for
<code>SCRAM-*</code> authentication (GHSA-87pf-fpwv-p7m7).</p>
<p>The default <code>ScramAuthenticator#max_iterations</code> is
<code>2**31 - 1</code> (max 32-bit signed int), which was already
OpenSSL's maximum value. <em>It provides no protection</em> against
hostile servers unless it is explicitly set to a lower value by the
user.</p>
</blockquote>
<h3>Added</h3>
<ul>
<li>🔒 Add <code>ScramAuthenticator#max_iterations</code> (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/654">#654</a>) in
<a
href="https://redirect.github.com/ruby/net-imap/pull/655">ruby/net-imap#655</a>,
reported by <a
href="https://github.com/Masamuneee"><code>@​Masamuneee</code></a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>🔒 Fix STARTTLS stripping vulnerability (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>) in
<a
href="https://redirect.github.com/ruby/net-imap/pull/666">ruby/net-imap#666</a>,
reported by <a
href="https://github.com/Masamuneee"><code>@​Masamuneee</code></a></li>
<li>🔒 Fix CRLF injection vulnerabilities (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/657">#657</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/658">#658</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/659">#659</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/660">#660</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/636">#636</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/661">#661</a>) in
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>,
reported by <a
href="https://github.com/manunio"><code>@​manunio</code></a></li>
<li> Much faster ResponseReader performance (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/642">#642</a>) in
<a
href="https://redirect.github.com/ruby/net-imap/pull/651">ruby/net-imap#651</a>,
reported by <a
href="https://github.com/Masamuneee"><code>@​Masamuneee</code></a></li>
<li>🐛 Wait to continue RawData literals (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/660">#660</a>) by
<a href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a></li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>♻️ Improve internal literal sending (partially backports <a
href="https://redirect.github.com/ruby/net-imap/issues/358">#358</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/616">#616</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/649">#649</a>) by
<a href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/653">ruby/net-imap#653</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/net-imap/compare/v0.4.23...v0.4.24">https://github.com/ruby/net-imap/compare/v0.4.23...v0.4.24</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="24a4e770b4"><code>24a4e77</code></a>
🔀 Merge pull request <a
href="https://redirect.github.com/ruby/net-imap/issues/666">#666</a>
from ruby/backport/v0.4/STARTTLS-stripping</li>
<li><a
href="63f53ffdef"><code>63f53ff</code></a>
🔖 Bump version to 0.4.24</li>
<li><a
href="038ae35d5e"><code>038ae35</code></a>
🍒 pick 24d5c773d: 🔒🥅 Handle tagged &quot;OK&quot; to incomplete command
[backport <a
href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>]</li>
<li><a
href="705aa59faa"><code>705aa59</code></a>
🍒 pick 62eea6ffe: 🔒🥅 Ensure STARTTLS tagged response was handled
[backport <a
href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>]</li>
<li><a
href="c9a6f28f87"><code>c9a6f28</code></a>
🍒 pick 46636cae8: 🔒 Add failing test for STARTTLS stripping [backport
<a
href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>]</li>
<li><a
href="aec06996eb"><code>aec0699</code></a>
🔀 Merge pull request <a
href="https://redirect.github.com/ruby/net-imap/issues/663">#663</a>
from ruby/backport/v0.4/raw_data-warnings</li>
<li><a
href="fd245ddd1e"><code>fd245dd</code></a>
🍒 pick be32e712e: 📚 Improve documentation of RawData arguments
[backports <a
href="https://redirect.github.com/ruby/net-imap/issues/661">#661</a>]</li>
<li><a
href="6dd110bfda"><code>6dd110b</code></a>
🍒 pick 47c72186d: 🐛 Validate RawData and wait to continue literals
[backports...</li>
<li><a
href="4e93149e65"><code>4e93149</code></a>
🔀 Merge branch 'backport/v0.4/QUOTA-argument-validation' into
backport/v0.4/s...</li>
<li><a
href="d2b23602e8"><code>d2b2360</code></a>
🍒 pick 0ec4fd351: 🥅 Validate <code>#setquota</code> storage limit
argument [backports <a
href="https://redirect.github.com/ruby/net-imap/issues/659">#659</a>]</li>
<li>Additional commits viewable in <a
href="https://github.com/ruby/net-imap/compare/v0.4.20...v0.4.24">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=net-imap&package-manager=bundler&previous-version=0.4.20&new-version=0.4.24)](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)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2026-05-14 19:51:01 -07:00
Sivin Varghese
dc332dd93e
feat: add attachments endpoint for contact media view (#14391)
# Pull Request Template

## Description

This PR adds an endpoint to fetch all attachments shared with or by a
contact across all of their conversations.

Results are scoped based on the access:
* Admins can access all attachments
* Agents can access attachments only from inboxes they belong to
* Custom role agents are further filtered based on their conversation
permissions

Each attachment payload includes `conversation_id`, allowing the UI to
deep-link back to the source conversation.

Added `GET
/api/v1/accounts/:account_id/contacts/:contact_id/attachments` under the
existing contacts scope.

Fixes
https://linear.app/chatwoot/issue/CW-7021/add-media-view-to-the-contact-details-page

## Type of change

- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-14 21:34:39 +05:30
Shivam Mishra
13f66e3a88
fix: incorrect scope across controllers (#14459)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-14 20:34:18 +05:30
Sojan Jose
fbcb89e955
fix(swagger): prevent path traversal in docs controller (#14458)
This hardens the development/test Swagger docs endpoint by ensuring
requested files are resolved only within the `swagger/` directory.

This did not affect production security because the Swagger controller
only renders files in development or test environments; production
already returns `404`. The change still closes the scanner finding and
prevents future automated reports from flagging the development-only
path.

## Closes

Addresses: GHSA-xhp7-ggjq-p2rg

## How to reproduce

1. Start Chatwoot locally in development.
2. Visit `/swagger/%2Fetc%2Fpasswd`.
3. Before this change, the endpoint could render files outside the
Swagger directory in development/test.

## What changed

- Resolve Swagger file requests relative to `Rails.root/swagger`.
- Return `404` when the resolved path is outside the Swagger directory
or does not point to a file.
- Strip leading slashes from derived request paths.
- Add a request spec for the encoded absolute-path case.

## How to test

1. Start the app locally.
2. Visit `/swagger` and confirm the ReDoc page loads.
3. Visit `/swagger/swagger.json` and confirm the Swagger JSON loads.
4. Visit `/swagger/%2Fetc%2Fpasswd` and confirm it returns `404` with no
file contents.

Note: `bundle exec rspec spec/controllers/swagger_controller_spec.rb`
was passing locally earlier during this fix. A final rerun before
opening the PR was blocked because local Postgres on `localhost:5432`
was not accepting connections.

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-05-14 18:52:14 +05:30
Shivam Mishra
8712879681
test: Stabilize SafeFetch spec against constant-identity flake (#14454)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
`spec/lib/safe_fetch_spec.rb` has been flaking intermittently under
full-suite runs with errors like:

```
expected SafeFetch::FileTooLargeError, got #<SafeFetch::FileTooLargeError: exceeded 1048576 bytes>
```

The class name on both sides is identical — yet RSpec reports a
mismatch. This PR replaces the constant-identity assertions in this spec
with a name-based matcher so the comparison stops depending on the live
Class object's identity. We have made a similar fix earlier, but that
wasn't addressing the core of the issue in #14139.


**How to reproduce:** The flake only surfaces under load. Running the
spec in isolation almost always passes, here's a [run failing in
CI](https://github.com/chatwoot/chatwoot/actions/runs/25852294516/job/75961520248?pr=14370)

## What's actually going on

Three facts combine:

1. **Test env reloads classes.** `config/environments/test.rb` sets
`cache_classes = false` so Zeitwerk reloads autoloadable code on demand.
2. **`lib/` is on the reloadable autoload tree.**
`config/application.rb` adds `lib` to `eager_load_paths`, which (with
`eager_load = false` in test) makes it lazily loaded by Zeitwerk's
*main* (reloading) autoloader. `lib/safe_fetch/` lives under that
umbrella.
3. **RSpec's `raise_error(Klass)` snapshots the Class object.**
`raise_error` matcher captures `Klass` (a specific `Class` instance)
when the matcher is built. At raise time it compares with `Module#===`,
which is identity-based.

When the executor between examples triggers
`Rails.application.reloader.reload!`, Zeitwerk does
`remove_const(:SafeFetch)` and re-installs an autoload trigger. The next
access produces a **fresh** `Class` object for
`SafeFetch::FileTooLargeError` — same name, different identity. The
matcher's snapshot now points at the dead Class, and the live raise
produces the new one. Identity fails, even though the error is
semantically correct.

This bites SafeFetch specifically because:

- SafeFetch is a *namespace* with 7 nested error classes — one reload
invalidates all of them.
- The spec contains 14+ `raise_error(SafeFetch::Foo)` assertions — many
chances to land in a reload window.
- SafeFetch is exercised by request-driven code (webhook delivery,
avatar fetch). Earlier specs in the suite warm up the reloader
machinery, which then fires during this spec.

Other custom-error specs don't visibly flake because their consumers
`rescue ConstName => e` (dynamic class lookup at raise time, walks the
ancestor chain via `Module#===` *at the moment of raise*, with no
captured snapshot), rather than RSpec's snapshot-then-compare pattern.

## What this PR does

Adds a small local helper to the spec that matches by class name string,
not by Class identity:

```ruby
def safe_fetch_error(name, message_pattern = nil)
  satisfy("raise SafeFetch::#{name}#{" matching #{message_pattern.inspect}" if message_pattern}") do |error|
    error.class.name == "SafeFetch::#{name}" &&
      (message_pattern.nil? || message_pattern.match?(error.message))
  end
end
```

Every `raise_error(described_class::FooError)` becomes
`raise_error(safe_fetch_error('FooError'))`. Class names are strings;
string equality survives any number of reloads. The semantic assertion
("this raised the right kind of error") is preserved.

This is the pattern CLAUDE.md already endorses for this codebase:

> Specs in parallel/reloading environments: prefer comparing
`error.class.name` over constant class equality when asserting raised
errors.

## Why this approach (even though it's suboptimal)

To be honest with reviewers: **this is a workaround, not a root-cause
fix.** Future spec authors who use `raise_error(SafeFetch::Foo)` in
other files will hit the same flake. The "real" fix removes the failure
mode at the source rather than dodging it per-spec.

Here's the menu of options we considered and the tradeoffs:

### Option A — Spec-side name matcher (this PR)

- **What:** the helper above.
- **Pro:** one-file change, zero blast radius beyond the spec.
- **Pro:** matches CLAUDE.md's documented stance.
- **Con:** every new spec touching reloadable error classes needs to
remember this pattern. It's a discipline tax, not a structural fix.
- **Verdict:** chosen for this PR.

### Option B — Pin SafeFetch outside Zeitwerk

```ruby
# config/application.rb
Rails.autoloaders.main.ignore(
  Rails.root.join('lib/safe_fetch.rb'),
  Rails.root.join('lib/safe_fetch'),
)
require Rails.root.join('lib/safe_fetch')
```

- **Pro:** real root-cause fix. `SafeFetch::*` constants become
process-lifetime stable. The spec helper becomes unnecessary, every
assertion in any spec works correctly.
- **Pro:** preserves the public API exactly.
- **Con:** loses hot-reload for SafeFetch in dev (need server restart to
see edits).
- **Con:** modifies `application.rb`, which has cross-team review
weight.
- **Verdict:** rejected for scope reasons in this PR; a sensible
follow-up.

### Option C — Move error classes to a non-reloadable location

Define top-level error classes in
`config/initializers/safe_fetch_errors.rb`. Initializers run once at
boot, constants are never reloaded.

- **Pro:** identity-stable errors, no spec helper needed.
- **Con:** API change — `SafeFetch::FileTooLargeError` becomes
`SafeFetchFileTooLargeError` (or similar). Every consumer's `rescue`
clause has to update.
- **Con:** namespace pollution at top-level.
- **Verdict:** rejected. The constraint of touching initializers is what
motivated the simpler PR.

### Option D — Vendor SafeFetch as a path gem

Move `lib/safe_fetch/` → `vendor/gems/safe_fetch/` with a gemspec.
Bundler `require`s gems once; Zeitwerk has zero involvement.

- **Pro:** structurally the most correct fix. Same identity stability as
Option B, no Zeitwerk plumbing required.
- **Pro:** zero API change for consumers.
- **Con:** larger refactor (6+ files moved, gemspec authored,
Gemfile/Gemfile.lock updated).
- **Con:** SafeFetch becomes harder to iterate on in dev (gem-style
edit-then-restart loop).
- **Verdict:** rejected for scope in this PR; the cleanest long-term
home for a security primitive.

### Option E — Drop custom errors, return a `Result`

Refactor SafeFetch to yield a result hash (`{ ok: true, ... }` / `{ ok:
false, kind: :unsafe_url, ... }`) instead of raising for anticipated
outcomes. Failure kinds become symbols, which are interned for the
process lifetime.

- **Pro:** eliminates the failure mode at its true root — there are no
custom exception classes to reload, anywhere.
- **Pro:** forces explicit, exhaustive handling at every call site — a
feature for a security primitive.
- **Pro:** spec assertions become data assertions (`expect(result).to
match(ok: false, kind: :too_large)`), which are robust against any
reload, any reordering.
- **Con:** paradigm shift away from the exception-driven style used
everywhere else in the codebase.
- **Con:** every consumer rewrites their `rescue` block into
pattern-matched handling.
- **Verdict:** rejected for scope in this PR; the architecturally
cleanest answer if we ever revisit SafeFetch's API.

## Why we're shipping A despite knowing B–E are better

This flake has been chewing CI time intermittently and previously took a
partial fix (#14139). We need it stable *now*. Options B–E are real
refactors with broader review surface (`application.rb`, consumer code,
or the lib's public contract). Option A:

- Costs nothing — one helper, mechanical replacements.
- Doesn't preclude any of B/C/D/E later. The helper goes away cleanly
once a root-cause fix lands.
- Aligns with the project's documented guidance for this scenario.

When SafeFetch next gets a substantive change, that's the right moment
to fold in B or D. Until then, the spec is stable and CI gets its time
back.

## What changed

- `spec/lib/safe_fetch_spec.rb`: added `safe_fetch_error(name,
message_pattern = nil)` helper; converted all 14
`raise_error(described_class::FooError[, /regex/])` assertions to
`raise_error(safe_fetch_error('FooError'[, /regex/]))`.
2026-05-14 16:48:14 +05:30