Commit Graph
6293 Commits
Author SHA1 Message Date
Muhsin KelothandGitHub 556830badb Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-06-12 10:30:04 +04:00
c041fde3a2 fix: Preserve original filenames for WhatsApp cloud attachments (#14168)
## Summary

Preserves the original filename provided in the WhatsApp Cloud
attachment payload when downloading media files.

## Problem

Files containing accented or special characters could be saved with
malformed names or incorrect extensions due to relying on remote
download metadata.

## Changes Made

- Uses attachment_payload[:filename] when present
- Creates a tempfile using the original filename and extension
- Preserves content type metadata
- Falls back to existing behavior when no filename is provided

## Notes

This should improve attachment downloads for filenames containing
accented characters.
Related to #10973

---------

Co-authored-by: Rorrick Smith <rorrick@bulksms.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-12 10:21:42 +04:00
c6a38e2fc6 fix: Keep Instagram scopes out of new Messenger OAuth flows (#14695)
Removes Instagram permissions from the Facebook Messenger inbox setup
flow to avoid blocking Meta App Review for Messenger-only apps.

Meta rejects or stalls Messenger-only app reviews when the OAuth flow
requests unrelated Instagram permissions such as `instagram_basic` and
`instagram_manage_messages`. This blocks approval for Messenger
permissions like `human_agent`, even when the user only wants to
configure a Facebook Messenger inbox.

New Facebook Messenger inbox setup now requests only the Page and
Messenger permissions required for Messenger. Existing Facebook
Messenger inboxes that already have Instagram details continue to
request Instagram permissions during reauthorization, preserving support
for the legacy combined Facebook/Instagram inbox flow.

Going forward, new Instagram setups should use the dedicated Instagram
channel inbox instead of being bundled into the Messenger setup flow.

Closes #13860

## How to test

1. Go to Settings → Inboxes → Add Inbox → Facebook Messenger.
2. Click Login with Facebook.
3. Verify the OAuth scope does not include `instagram_basic` or
`instagram_manage_messages`.
4. Reauthorize an existing Facebook Messenger inbox with `instagram_id`
present.
5. Verify the reauth scope still includes `instagram_basic` and
`instagram_manage_messages`.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-12 09:39:26 +04:00
Sugar Ray LedesmaandGitHub 2663a8495d refactor(security): rename path_without_extensions in Rack::Attack (#14216)
Fix misspelling 'extentions' and clarify comments. Behavior unchanged:
same path-stripping logic for throttle matching (e.g. /auth and
/auth.json). Also correct 'You may' in remote_ip comment.
2026-06-11 18:36:34 +05:30
Sivin VargheseandGitHub d0d275d962 fix: cannot resize pasted images (#14709) 2026-06-11 16:47:11 +05:30
95d6aecb51 chore: Add security flags to session cookie configuration (#14248)
## Summary
- Adds `httponly` and `secure` flags to session cookie configuration
- Adds RSpec tests for session configuration

## Changes
- `config/initializers/session_store.rb`: add `httponly: true`, `secure:
FORCE_SSL`
- `spec/config/session_store_spec.rb`: tests for all session store
options

Ref #2683 (hardens the session cookie but does not remove it, sessions
are still needed for super_admin dashboard)

---------

Co-authored-by: Adam-Relay <adam@userelay.ai>
Co-authored-by: Adam Berger <adam@adam-berger.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-06-11 16:13:57 +05:30
a901c87ab4 perf: merge 3 conversation COUNT queries in /meta into single query (#13536)
# Pull Request Template

## Description


- Merges 3 separate COUNT queries in
`ConversationFinder#set_count_for_all_conversations` into a single query
using PostgreSQL `COUNT(*) FILTER (WHERE ...)`
- Every call to `/meta` and conversation index fires 3 COUNT queries
against the conversations table — one for "mine", one for "unassigned",
one for "all". These share the same base query and scan the same rows,
but execute as 3 independent round-trips.

from pg_stats

| Query | Calls | Total DB Time | Avg Latency |
|-------|-------|--------------|-------------|
| COUNT unassigned | 4.1M | 141,081 sec | 34ms |
| COUNT all | 4.1M | 132,302 sec | 32ms |
| COUNT mine | 3.7M | 37,637 sec | 10ms |
| **Total** | **~12M** | **311,020 sec** | |

`/meta` alone runs at 2.3K RPM (NewRelic), triggered on every WebSocket
event (conversation created/updated/status changed/assignee changed).



Fixes
https://linear.app/chatwoot/issue/INF-43/make-single-query-to-count-the-assigned-unassigned-and-all-instead-of

## Type of change

- [x] Performance fix

## How Has This Been Tested?

- [x] Verify conversation list shows correct
mine/unassigned/assigned/all counts
- [x] Verify /meta endpoint returns correct counts
- [x] Verify counts update correctly when assigning/unassigning
conversations
- [x] Verify counts with team filter applied
- [x] Monitor DB query count reduction in NewRelic after deploy

## 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: Sony Mathew <ynos1234@gmail.com>
2026-06-11 16:10:48 +05:30
Sony MathewandGitHub 6d26e7930a fix(contacts): truncate inbound contact names (IMAP sender display names) (#14631)
## Description

Truncates long contact names in `ContactInboxWithContactBuilder` before
persisting them, so inbound IMAP sender display names over the
`contacts.name` 255-character limit no longer raise
`ActiveRecord::RecordInvalid` from `Inboxes::FetchImapEmailsJob`. This
keeps email ingestion moving while preserving the existing contact name
length constraint.

Linear ticket:
[https://linear.app/chatwoot/issue/CW-7263/sentry-active-record-invalid-contact-create-from-imap-fetch-job](https://linear.app/chatwoot/issue/CW-7263/sentry-active-record-invalid-contact-create-from-imap-fetch-job)

## Type of change

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

## How Has This Been Tested?

- [x] `bundle exec rspec
spec/builders/contact_inbox_with_contact_builder_spec.rb`
- [x] `bundle exec rspec spec/mailboxes/imap/imap_mailbox_spec.rb`
- [x] `bundle exec rubocop
app/builders/contact_inbox_with_contact_builder.rb
spec/builders/contact_inbox_with_contact_builder_spec.rb`

## 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
- [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
2026-06-11 15:15:14 +05:30
Tanmay Deep SharmaandGitHub 8d5d02ea97 feat: add per-inbox toggle to disable incoming calls (#14645)
## Description

Adds a per-inbox "Allow incoming calls" toggle for voice-enabled
WhatsApp and Twilio inboxes. When turned off, the setting is persisted
on the channel; actually rejecting inbound calls is handled in a
follow-up PR.

## Type of change

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

## Screenshot
<img width="804" height="384" alt="Screenshot 2026-06-04 at 11 44 07 AM"
src="https://github.com/user-attachments/assets/df8bb026-0387-4031-bcba-6d9a56872eb7"
/>


## 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-06-11 14:22:44 +05:30
ce93ddec78 fix: re-fetch widget conversation status on reconnect (#14683)
When an inbox has **"Allow messages after conversation is resolved"**
disabled, the live-chat widget should hide the reply box once a
conversation is resolved — but users could still send a reply, silently
reopening it. This syncs the widget's conversation status on reconnect
so the reply box hides correctly.

## Closes
- CW-7272

## How to reproduce
1. Disable **Allow messages after conversation is resolved** on an
inbox.
2. Open the widget, then let its websocket drop (background tab / lose
network).
3. While disconnected, get the conversation resolved (e.g. auto-resolve
on inactivity).
4. Reconnect → reply box is still visible and a sent message reopens the
resolved conversation.

## Why
Reply-box hiding is gated on the widget's local status, updated via the
`conversation.status_changed` socket event. If the resolve happens while
disconnected, the event is missed — and on reconnect the widget only
re-synced messages, never the status, so it stayed stale at `open`.

## What changed
- `onReconnect` now also dispatches
`conversationAttributes/getAttributes` to refresh status after a missed
event.
- Added a spec covering the reconnect behavior.

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-11 14:09:13 +05:30
Sivin VargheseandGitHub 940428c10e fix: preserve markdown links with unsafe href characters in help center content (#14686) 2026-06-11 13:47:08 +05:30
4d3196b02f feat: Show unread count for all conversations (#14627)
## Description

Adds the unread count for All Conversations to the left sidebar. The
unread counts API now returns an `all_count` aggregate based on the same
permission-scoped inbox counts already used for sidebar badges, and the
sidebar renders that backend-provided value on the All Conversations
item.

Fixes
[CW-7240](https://linear.app/chatwoot/issue/CW-7240/unread-count-for-all-conversations)

## Type of change

- [ ] 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?

- `bundle exec rspec
spec/services/conversations/unread_counts/counter_spec.rb
spec/enterprise/services/conversations/unread_counts/counter_spec.rb
spec/controllers/api/v1/accounts/conversations_controller_spec.rb`
- `pnpm test
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js`
- `pnpm exec eslint
app/javascript/dashboard/components-next/sidebar/Sidebar.vue
app/javascript/dashboard/store/modules/conversationUnreadCounts.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js`
- `bundle exec rubocop
app/services/conversations/unread_counts/counter.rb
spec/services/conversations/unread_counts/counter_spec.rb
spec/enterprise/services/conversations/unread_counts/counter_spec.rb
spec/controllers/api/v1/accounts/conversations_controller_spec.rb`

## 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: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-11 13:05:52 +05:30
Tanmay Deep SharmaandGitHub c6e5657ee5 fix(call): emit assignee activity message when agent answers a call (#14700)
## Linear Ticket
- https://linear.app/chatwoot/issue/CW-7249/debug-assignment-log

## Description

When an agent answers an inbound WhatsApp call on an unassigned
conversation, the conversation is claimed for that agent — but no
"assigned" activity message or assignee-changed event was emitted, so
the assignment was invisible in the timeline (and notifications/live
assignee panel didn't update).

The claim ran before `update_conversation_call_status`, so that later
`update!` on the same conversation clobbered
`saved_change_to_assignee_id?` before `after_commit` fired. Moving the
claim to be the conversation's final write in the transaction restores
the activity message, the `ASSIGNEE_CHANGED` event, and the live
assignee update.

## Type of change

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

## How Has This Been Tested?

1. Open an unassigned WhatsApp-call conversation.
2. As an agent, answer an inbound call.
3. Before: the conversation is assigned to you, but no "self-assigned"
activity message appears. After: the activity message is created and the
assignee updates live.

## 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-06-11 11:16:41 +05:30
Sony Mathew e9a92a6366 Merge branch 'release/4.14.2' into develop 2026-06-10 22:44:31 +05:30
Sony Mathew bec795f764 Bump version to 4.14.2 2026-06-10 22:43:22 +05:30
Shivam MishraandGitHub e6dfb91fcc refactor: use SafeFetch for website branding page fetch (#14693) 2026-06-10 16:26:24 +05:30
Shivam MishraandGitHub d9c07fe2e9 feat: expose onboarding help center generation status (#14671)
This PR adds a reload-safe onboarding Help Center generation status
path. Previously, generation progress was pushed through ActionCable,
which meant the onboarding UI could lose context after a page reload or
missed websocket event. The new endpoint exposes the current generation
id, raw Redis generation state, and Help Center article/category counts
so clients can recover state by fetching from the backend.

**What changed**

- Added an onboarding Help Center generation status endpoint.
- Persisted `help_center_generation_id` when generation is enqueued.
- Removed Help Center generation ActionCable broadcasts completely.
- Kept generation progress in Redis as the backend source of truth.
- Kept Help Center generation out of the onboarding hot path; the
existing onboarding controller does not start generation yet.

**How to test**

1. Start Help Center generation for an account.
2. Fetch the onboarding generation status endpoint and verify it returns
the generation id, Redis state, and article/category counts.
3. Reload the onboarding UI or client state and fetch again to confirm
progress can be recovered without relying on websocket events.
4. Verify skipped/completed generation states are reflected from Redis.

## Related PRs

- https://github.com/chatwoot/chatwoot/pull/14569
- https://github.com/chatwoot/chatwoot/pull/14568
- https://github.com/chatwoot/chatwoot/pull/14567
- https://github.com/chatwoot/chatwoot/pull/14619
- https://github.com/chatwoot/chatwoot/pull/14565 (Primary onboarding
PR)
2026-06-10 16:24:37 +05:30
f93f2067b6 fix(whatsapp): Drop obsolete WABA scope check broken by Meta embedded signup (#14697)
## Description

Fixes WhatsApp embedded signup failing with "No WABA scope found in
token." Meta recently changed which scopes embedded-signup tokens carry
(whatsapp_business_manage_events instead of
whatsapp_business_management), which tripped a brittle scope-string
check. That check was redundant anyway — PhoneInfoService already
verifies the token's access to the specific WABA by calling its
/phone_numbers endpoint right before it. This removes the obsolete
TokenValidationService and relies on the functional check.

## Type of change

- [ ] 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

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-10 13:22:30 +04:00
Shivam MishraandGitHub 3dfb5061e1 refactor: route captain custom tool requests through SafeFetch (#14620)
Captain custom tools previously used their own hand-rolled `Net::HTTP`
request code. This moves them onto `SafeFetch`, the same shared
HTTP-fetching helper already used by webhooks, uploads, avatar imports,
and the Captain page crawler. Behavior for normal tools is unchanged —
they just now share one consistent path for host resolution, timeouts,
response size limits, and redirect handling.

**What changed**
- `HttpTool#execute_http_request` now delegates to `SafeFetch.fetch`
instead of building `Net::HTTP` requests by hand. Auth headers, basic
auth, metadata headers, JSON content-type, and the 1 MB response cap all
map onto `SafeFetch` options.
- Removed ~80 lines of bespoke request/validation plumbing from
`HttpTool`.
- The custom tools `test` endpoint now reads the response body string
directly (the executor returns the body rather than a response object).

**How to test**
1. Enable `custom_tools` (or `captain_integration_v2`) for an account.
2. Create a Captain custom tool pointing at a public HTTPS endpoint
(e.g. a test API).
3. Use the **Test** button in the tool form — you should get a success
result.
4. Run the tool from a Captain conversation and confirm the response is
returned/templated as before.

**Gotchas**
- **Local dev:** `SafeFetch` blocks requests to private/loopback
addresses by default. If you're testing a custom tool against a service
on `localhost` or a private IP during development, set
`SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true` or the request will be rejected.
(This matches how the rest of `SafeFetch` already behaves locally.)
- **Test endpoint status field:** on success the `test` response now
reports `status: 200` rather than the exact 2xx code (201/204/etc.),
because `SafeFetch` signals success-vs-failure rather than exposing the
raw response. The UI only checks the 2xx range, so this is invisible
there — but worth knowing if anything consumes the API directly.
- **Non-2xx responses** still surface as an error (same as before), now
via `SafeFetch::HttpError`.
2026-06-10 14:01:30 +05:30
72a59e4795 fix: update oauth dependencies (#14691)
Updates the locked OAuth dependencies to patched versions so bundle
audit no longer reports the current OAuth advisories.

What changed
- Updated `oauth` from `1.1.0` to `1.1.6` for `GHSA-prq8-7wvh-44qh`.
- Updated `oauth2` from `2.0.9` to `2.0.22` for `GHSA-pp92-crg2-gfv9`.
- Accepted Bundler's required transitive lockfile updates for the
patched OAuth gems.

How to test
1. Run `bundle exec bundle audit update && bundle exec bundle audit
check -v` and confirm no vulnerabilities are reported.
2. Smoke test OAuth-based authentication and email integration flows.

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-06-10 12:33:46 +05:30
Sivin VargheseandGitHub cabe9bc733 fix: populate general settings form on hard reload (#14685) 2026-06-10 10:56:17 +05:30
33f7550525 fix(whatsapp): restrict OGG voice recording to WhatsApp Cloud inboxes (#14692)
Recording and sending an audio message from a **Twilio WhatsApp** inbox
failed silently — Twilio rejected the media with delivery error `63019`
("Media failed to download") and the voice note never reached the
customer. This restores audio sending for Twilio WhatsApp (and
360dialog) inboxes.

Closes Regression from #14606

## How to reproduce

1. Open a conversation in a **Twilio WhatsApp** inbox.
2. Record a voice message in the reply box and send it.
3. Before this fix: the message fails to deliver and a
`Webhooks::TwilioDeliveryStatusJob` is enqueued with `ErrorCode: 63019`,
`ErrorMessage: "Media failed to download"`.
4. After this fix: the audio is recorded as MP3 and delivers normally.

## What changed

PR #14606 added WhatsApp **Cloud** voice notes, which require OGG/Opus.
It changed `audioRecordFormat` in `ReplyBox.vue` to return OGG for
`isAWhatsAppChannel` — but that getter is also `true` for Twilio
WhatsApp inboxes. The OGG handling (content-type normalization + the
`voice: true` flag) lives only in `WhatsappCloudService`, so Twilio
could not download/process the remuxed OGG file.

This change scopes OGG to `isAWhatsAppCloudChannel`. Twilio WhatsApp,
360dialog, and Telegram fall back to MP3 exactly as they did before the
PR.

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

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 08:52:34 +04:00
Sivin VargheseandGitHub 59d869d1ed feat: Ability to resize table column width (#14611) 2026-06-09 18:04:48 +05:30
8a3b129292 fix: Disable re-oauth flow for manual whatsapp (#13599)
Restrict the WhatsApp reauthorization flag to channels whose
provider_config source is 'embedded_signup'. Previously the view exposed
resource.channel.reauthorization_required? for all WhatsApp channels;
now it only returns true when (provider_config || {}).to_h['source'] ==
'embedded_signup' && resource.channel.reauthorization_required?. This
prevents showing reauthorization prompts for manual/API-key flows
(non-OAuth) and safely handles nil provider_config.

# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes  #13553 

## 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?

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
- [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
- [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: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-09 11:12:03 +04:00
af1dfc21f6 fix: include account data in webhook payloads (#12445)
Conversation and inbox webhook payloads now include the account object
(`{ id, name }`) so integrations can reliably identify the account
without depending on nested message data.

## Closes

Closes #11753
Closes #12442

## Why

Message, contact, and contact inbox webhook payloads already expose
`account`. Conversation and inbox webhook payloads were outliers, which
forced webhook consumers to infer account context from nested messages
or other fields that may not always be present.

## What changed

- Adds `account: { id, name }` to conversation webhook payloads.
- Adds webhook-specific inbox payload data with `account: { id, name }`
for inbox created/updated events.
- Keeps non-webhook conversation and inbox push payload behavior
unchanged.

## How to test

- Configure an account webhook for `conversation_created`, trigger a new
conversation, and confirm the webhook payload includes `account.id` and
`account.name`.
- Configure an account webhook for `inbox_created`, create a new inbox,
and confirm the webhook payload includes `account.id` and
`account.name`.
- Configure a webhook agent bot and update a conversation status, then
confirm the bot webhook payload includes the same account object.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-06-09 12:12:46 +05:30
d1c482cb64 fix(email): strip null bytes from inbound mailbox messages (#14546)
Inbound emails containing null bytes could fail while Chatwoot persisted
the incoming message, causing IMAP sync to drop and repeatedly retry the
same malformed email. This strips null bytes at the inbound mailbox
message persistence boundary so the rest of the email can be saved
normally.

Fixes
https://linear.app/chatwoot/issue/CW-7165/argumenterror-string-contains-null-byte-argumenterror
Sentry: https://chatwoot-p3.sentry.io/issues/7405946944/

## Test
- [x] added specs
- [x] manually tested the logic on prod for the inbox(email) which was
throwing the error

## What changed

- Sanitizes null bytes only while building attributes for inbound
mailbox message persistence.
- Uses the sanitized source ID for both duplicate lookup and message
creation.
- Adds regression coverage for the mailbox message creation path.

---------

Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-08 17:12:25 +05:30
d8656edc61 fix(facebook): Stop force tagging Messenger replies with MESSAGE_TAG/ACCOUNT_UPDATE (#14329)
Outbound Facebook Messenger replies were always sent with
`messaging_type: MESSAGE_TAG` and a `tag` of either `HUMAN_AGENT` or
`ACCOUNT_UPDATE` (fallback). `ACCOUNT_UPDATE` is reserved by Meta policy
for non-recurring account notifications (password resets, suspicious
activity, account-status changes), so general agent replies were getting
rejected by Facebook with "Invalid parameter" and risked page-level
penalties. After this fix, Facebook treats default replies as standard
`RESPONSE` messages within the 24-hour window, and only attaches
`HUMAN_AGENT` tagging when the operator opts in via
`ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT`.

## Closes

<!-- Add the relevant issue link, e.g. Closes #1234 -->

## How to reproduce

1. Connect a Facebook page inbox.
2. Leave `ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT` disabled (default).
3. From the dashboard, reply to an active Messenger conversation with a
regular/promotional message.
4. Before this PR: the request to Graph API includes
`messaging_type=MESSAGE_TAG&tag=ACCOUNT_UPDATE`. Facebook returns
"Invalid parameter" for content that does not match the `ACCOUNT_UPDATE`
use case, and the message is marked failed in Chatwoot.
5. After this PR: the request omits `messaging_type` and `tag`, so
Facebook treats it as a standard `RESPONSE` and accepts it within the
24-hour customer service window.

## What changed

- `app/services/facebook/send_on_facebook_service.rb` now mirrors the
Instagram service pattern: text and attachment params are built without
`messaging_type`/`tag` by default, and a shared `merge_human_agent_tag`
helper attaches `MESSAGE_TAG`/`HUMAN_AGENT` only when
`ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT` is enabled.
- Removed the unused `sent_first_outgoing_message_after_24_hours?`
helper (dead code with no callers).
- Updated `spec/services/facebook/send_on_facebook_service_spec.rb`:
dropped `ACCOUNT_UPDATE` expectations, added a spec asserting no
`messaging_type`/`tag` is sent by default, and tightened the HUMAN_AGENT
spec to verify both `messaging_type` and `tag`.

## Operator note

Operators who were relying on the prior `ACCOUNT_UPDATE` fallback to
bypass the 24-hour window were already in violation of Meta policy and
likely seeing send failures. They should enable
`ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT` (and request the Human Agent
permission from Meta) for the 7-day extended window

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-08 15:28:36 +04:00
e919a2cef5 feat: add contact filter for conversations (#14629)
# Pull Request Template

## Description

Adds a Contact condition to the conversation advanced filter so agents
can search for an existing contact and filter conversations by
`conversations.contact_id`.

Fixes
[CW-7239](https://linear.app/chatwoot/issue/CW-7239/contact-filter-for-conversations)

## Type of change

- [ ] 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?

- `eval "$(rbenv init -)" && bundle exec rspec
spec/services/conversations/filter_service_spec.rb`
- `pnpm exec vitest --no-watch --no-cache --no-coverage --logHeapUsage
app/javascript/dashboard/components-next/filter/helper/filterHelper.spec.js
app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js
app/javascript/dashboard/helper/specs/customViewsHelper.spec.js
app/javascript/dashboard/helper/specs/filterQueryGenerator.spec.js`
- `pnpm eslint`
- `eval "$(rbenv init -)" && bundle exec rubocop
spec/services/conversations/filter_service_spec.rb`

## 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: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-08 16:00:17 +05:30
Sojan JoseandGitHub 7718f2a62c fix: update puma for security advisories (#14670)
Updates Puma to the patched 7.2 line so bundle-audit no longer reports
the high-severity Puma advisories affecting the current lockfile.

Fixes: CVE-2026-47736, CVE-2026-47737
Closes: N/A

## Why

The refreshed ruby-advisory-db flagged Puma 6.4.3 for two high-severity
PROXY Protocol v1 parser advisories. The advisory-recommended fixed
versions are the Puma 7.2 patch line or Puma 8, so this keeps the update
to the smaller fixed line.

## What changed

- Constrains Puma to `~> 7.2`, `>= 7.2.1`
- Updates the lockfile from Puma 6.4.3 to 7.2.1
- Updates nio4r from 2.7.3 to 2.7.5 as the transitive Puma dependency

## Validation

- `bundle exec bundle-audit check` -> `No vulnerabilities found`
- `bundle exec rails runner 'puts "Rails #{Rails.version}; Puma
#{Puma::Const::PUMA_VERSION}; Rack #{Rack.release}"'` -> `Rails 7.1.5.2;
Puma 7.2.1; Rack 3.2.6`
- `bundle exec rails server -p 3051` booted Puma 7.2.1 successfully
- Smoke checked `/`, `/api`, unknown-route 404, and `/cable` WebSocket
upgrade
2026-06-08 15:40:43 +05:30
Aakash BakhleandGitHub 45f4b423ae fix: captain usage for BYOK OpenAI tasks (#14587)
# Pull Request Template

## Description

Fixes:
https://linear.app/chatwoot/issue/CW-7167/label-suggestions-bad-ux

## 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.


## 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-06-08 13:37:21 +05:30
Shivam MishraandGitHub 78a6b2457d refactor(onboarding): extract whatsapp and facebook channel connect into reusable composables (#14619)
Extracts the Meta JS SDK logic for the WhatsApp embedded-signup and
Facebook Page connect flows out of their settings components into two
reusable composables. No behavior change — the settings inbox-creation
flows work exactly as before; this just makes the SDK orchestration
reusable (an upcoming onboarding PR consumes them) and adds unit
coverage.

## What changed
- `useWhatsappEmbeddedSignup` — owns the embedded-signup popup (SDK
load, FB.login, and the auth-code / postMessage race).
`WhatsappEmbeddedSignup.vue` now consumes it.
- `useFacebookPageConnect` — owns FB.login (page scopes) + page fetch,
with SDK preload split from login so the popup opens within the click's
activation window. `Facebook.vue` now consumes it.
- Adds unit specs for both composables.

## Related PRs

- https://github.com/chatwoot/chatwoot/pull/14569
- https://github.com/chatwoot/chatwoot/pull/14568
- https://github.com/chatwoot/chatwoot/pull/14567
- https://github.com/chatwoot/chatwoot/pull/14649
- https://github.com/chatwoot/chatwoot/pull/14565 (Primary onboarding
PR)
2026-06-08 13:13:20 +05:30
Shivam MishraandGitHub 73ba0b26e5 fix(instagram): process webhook events after mutex retry exhaustion (#14647)
Instagram webhooks use a Redis mutex to protect the first-message path
for a contact/account pair. When multiple webhook events for the same
Instagram contact arrive at nearly the same time, they can all enter the
“find or create” flow together.

There are two pieces involved:

`ContactInbox` maps the external Instagram scoped user id to a Chatwoot
contact inside an inbox. That side already has a unique `(inbox_id,
source_id)` index and retry handling, so duplicate contact-inbox
creation is mostly protected at the database layer.

`Conversation` creation is more fragile. The message builder looks for
an existing active conversation for the contact/inbox and creates one
when none is found. If two first-message jobs run concurrently, both can
see “no active conversation yet” and both can create a conversation. The
mutex exists to bump those jobs apart long enough for the first one to
create the conversation, so the next one appends to it instead of
creating a duplicate.

In production, the old behavior could turn that race dampener into a
drop path. Once `Webhooks::InstagramEventsJob` exhausted its lock
retries, ActiveJob stopped retrying with
`MutexApplicationJob::LockAcquisitionError`. Since ActiveJob retries are
not FIFO, later jobs could still win the lock while older jobs kept
getting deferred until exhaustion.

## Mutex Application Job Changes

This adds `retry_on_lock_conflict` as a small wrapper around the
existing `retry_on LockAcquisitionError` pattern.

The new API keeps the default behavior intact, but lets jobs explicitly
define what should happen after lock retry exhaustion:

```ruby
retry_on_lock_conflict wait: 1.second,
                       attempts: 3,
                       on_exhaustion: :process_without_lock
```

The fallback receives the original job arguments, so job classes do not
need to reach into ActiveJob internals like `arguments.first`.

## Instagram Fallback

Instagram now processes the webhook payload even after the mutex retry
window is exhausted.

This matches what the mutex was meant to do in the first place: bump
concurrent events apart long enough to avoid duplicate conversation
creation, not permanently block or drop webhook events. If a job cannot
acquire the lock after a few retries, it continues through the normal
Instagram processing path without the mutex.

## Retry And Lock Tuning

The Instagram lock retry window now uses deterministic backoff:

```ruby
retry_on_lock_conflict wait: ->(executions) { executions.seconds },
                       attempts: 3,
                       on_exhaustion: :process_without_lock
```

The lock TTL is also set explicitly:

```ruby
with_lock(key, 3.seconds)
```

This matters because Rails treats `attempts` as total executions, not
retries after the first execution. With a fixed `wait: 1.second` and
`attempts: 3`, the exhaustion handler can run roughly two seconds after
the first lock conflict. That is earlier than a 3-second lock TTL, so
the fallback could process without the lock while the original mutex is
still valid. That would reopen the duplicate-conversation race the lock
is meant to dampen.

Using a proc wait makes the timing predictable and avoids Rails jitter
for this retry path. The first conflict waits about 1 second, the second
waits about 2 seconds, and the final attempt happens around the 3-second
mark. That lines the retry window up with the Redis lock TTL before
falling back to `process_without_lock`.

The protected race window is intentionally small. `ContactInbox`
creation already has database uniqueness protection, while conversation
creation is the softer `find active conversation || create` path. We
only need to give the first job enough time to create the conversation
state that later jobs should reuse.

The mutex still does not preserve message order, and ActiveJob retries
are not FIFO. A longer retry window would mostly add latency for hot
Instagram contacts without making ordering more correct. After the lock
TTL has elapsed, processing without the lock is the better tradeoff than
dead-lettering customer messages.
2026-06-05 14:06:45 +05:30
f9385a31fc fix(api): allow agent bots to read conversations and manage labels (#14655)
Agent bots assigned to a conversation can now fetch that conversation's
details and manage its labels using their bot token. Previously these
two operations returned `Access to this endpoint is not authorized for
bots`, even though the same token worked for sending messages, updating
the conversation, and setting custom attributes.

Fixes
https://linear.app/chatwoot/issue/PLA-174/agent-bot-tokens-cannot-fetch-conversation-details-or-manage-labels

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:01:25 +04:00
9d808a18df fix(drops): Resolve first_name for single-word contact and agent names (#13488)
WhatsApp template variables like `{{contact.first_name}}` previously
resolved to an empty string whenever a contact or agent had a
single-word name (e.g. "Petterson"). Because the variable came back
blank, WhatsApp template sends would either fail validation or get stuck
indefinitely in the "Sending" state. With this fix, `first_name` falls
back to the full single-word name, so templates render correctly for
everyone regardless of how many words their name has.

## Closes

- Fixes #13222

## How to reproduce

1. Create a contact with a single-word name (e.g. `Petterson`).
2. Configure a WhatsApp template action that uses
`{{contact.first_name}}`.
3. Send the template message.
4. **Before:** the variable resolves to an empty string and the message
fails / stays in "Sending".
**After:** the variable resolves to `Petterson` and the message sends.

## What changed

- Removed the "name must have 2+ words" guard from `first_name` in
`ContactDrop` and `UserDrop`, so a single-word name is returned (still
capitalized).
- Capitalization behavior introduced in #6758 is preserved; only the
single-word edge case changes.
- `last_name` still returns `nil` for single-word names, which is the
expected behavior.
- Added specs covering single-word names for both `ContactDrop` and
`UserDrop`.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-05 10:03:24 +04:00
Sivin VargheseandGitHub ec43975f3f chore: use square avatars (#14656) 2026-06-05 11:25:32 +05:30
64c5aeebee feat(help-center): support per-locale portal title, name & header (#14642)
Portals can now override their name, page title and header text per
locale, with a fallback chain of locale override → default locale → base
value. Overrides are stored under portal.config.locale_translations and
validated with a JSON schema.

Editing is exposed as a "Localize content" action in each non-default
locale's menu on the Locale page, and all public-facing portal views
(classic + documentation layouts) render the localized values. The live
chat widget is also loaded in the active portal locale.


<img width="494" height="395" alt="Screenshot 2026-06-03 at 2 48 59 PM"
src="https://github.com/user-attachments/assets/e55a06e8-f20f-4d8a-9352-92fde28a9a19"
/>



https://github.com/user-attachments/assets/f5affbd2-7ad4-415a-b376-57c759fc2aaa

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 06:34:12 -07:00
Muhsin KelothandGitHub 86b7b4b046 Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-06-04 10:48:39 +04:00
cd9192f7d1 chore(captain): update Firecrawl to use the v2 API (#14624)
## Description

Migrates Firecrawl from the v1 to the v2 API.
`Captain::Tools::FirecrawlService` now targets `api.firecrawl.dev/v2`,
with the request body updated to match the v2 schema.

> Disclosure: I work at Firecrawl.

Fixes # (n/a)

## Type of change

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

## How Has This Been Tested?

Updated
`spec/enterprise/services/captain/tools/firecrawl_service_spec.rb` to
assert the v2 endpoint and request body.

## 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
- [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

---------

Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-06-03 23:47:49 +05:30
Aakash BakhleandGitHub eaffad12e7 feat(langfuse): propagate observation metadata for evals (#14634)
# Pull Request Template

## Description

We need to pass on trace level attributes down to the spans inside them
like tool calls, observations, etc.
This way, we can filter observations based on trace level attributes.


## 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.

Attributes added to observation metadata for easy filtering
<img width="1327" height="708" alt="image"
src="https://github.com/user-attachments/assets/8f1d1bf8-cde4-481d-a2c2-7920ad2fc52e"
/>

added a `generation_stage` to differentiate llm_calls that call tools vs
those that generate a `final_response`
<img width="1806" height="968" alt="CleanShot 2026-06-03 at 15 11 09@2x"
src="https://github.com/user-attachments/assets/db1fa8e0-7f2d-404b-a719-27a16d400442"
/>


propagated attributes to tool calls for future use
<img width="903" height="517" alt="image"
src="https://github.com/user-attachments/assets/edc61ce8-93db-465c-a66e-043138e2dc15"
/>



## 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-06-03 16:45:19 +05:30
Sivin VargheseandGitHub 18ef019cd4 fix: improve article editor typography & nested lists (#14572) 2026-06-03 16:11:32 +05:30
Tanmay Deep SharmaandGitHub ea910227ac feat: add Voice Calls feature card to settings showcase (#14635)
## Linear Ticket 

https://linear.app/chatwoot/issue/CW-7250/add-voice-calls-to-self-hosted-super-admin-feature-list

## Description

Adds a **Voice Calls** card to the Super Admin → Settings → Features
showcase so self-hostedadmins can see at a glance whether voice calling
is available on the installation.

## Type of change

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

## Screenshots?

<img width="1512" height="843" alt="Screenshot 2026-06-03 at 3 15 22 PM"
src="https://github.com/user-attachments/assets/988db14b-fef1-4b72-a55e-c7a8884521dd"
/>


## 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-06-03 16:01:14 +05:30
Tanmay Deep SharmaandGitHub 94ddd98050 chore: update the voice call ringtone (#14636)
## Linear Ticket
https://linear.app/chatwoot/issue/CW-7260/update-the-voice-call-ringtone

## Description

Updates the incoming voice call ringtone. The dashboard call widget now
plays the new tone while an inbound call is unanswered, replacing the
previous bell sound.

## 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
- [x] 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-06-03 16:00:04 +05:30
Tanmay Deep SharmaandGitHub 0e87519ecd feat: add mute button for Twilio calls (#14637)
## Linear Ticket

https://linear.app/chatwoot/issue/CW-7264/add-mute-button-in-twilio-calls

## Description

Adds mute support for Twilio voice calls. Previously the mute button was
shown only for WhatsApp calls (which toggle the local mic track in the
browser), so Twilio calls had no way to mute. The call widget now routes
mute by provider: WhatsApp continues to toggle the local mic track,
while Twilio uses the Voice SDK connection's native `mute()`. The mute
button is now shown for any active call, and unmute works symmetrically.

## 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
- [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
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-03 15:59:23 +05:30
Tanmay Deep SharmaandGitHub de89391031 fix: use hang-up handset icon for reject/end call button (#14640)
## Linear Ticket
https://linear.app/chatwoot/issue/CW-7261/call-window-theme-alignment

## Description

Updates the call window's reject/end button to use the hang-up handset
icon (the same handset as the accept button, rotated) instead of the
phone-with-X, matching the design.

## Type of change

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

## Screenshots

<img width="346" height="135" alt="Screenshot 2026-06-03 at 3 41 10 PM"
src="https://github.com/user-attachments/assets/4d507141-332e-436a-a6ec-516964a31013"
/>


## 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
2026-06-03 15:59:12 +05:30
Sojan JoseandGitHub 8e42307bdc fix: improve email inbox IMAP and SMTP compatibility (#14589)
Fetch IMAP message content using `BODY.PEEK[]` instead of `RFC822` to
avoid provider-specific parser failures while preserving unread state.
This also applies the existing SMTP timeout configuration to custom SMTP
email-channel replies, so provider SMTP responses have enough time to
complete.

Fixes: https://github.com/chatwoot/chatwoot/issues/12762

## Why

Some IMAP providers can return responses for `FETCH RFC822` that Ruby
`net-imap` fails to parse with:

`Net::IMAP::ResponseParseError: unexpected RPAR (expected ATOM or NIL)`

We reproduced this with iCloud IMAP. Authentication, `INBOX` selection,
and header fetches worked, but fetching full message content with
`RFC822` failed before Chatwoot received a `Mail::Message`.

The same mailbox successfully returned full message content when fetched
with `BODY.PEEK[]`.

> During end-to-end iCloud validation, inbound fetch worked after the
IMAP change, but outbound replies through the custom SMTP settings could
still fail with a socket read timeout. The OAuth SMTP path already used
explicit SMTP timeout values; the custom SMTP path was relying on mailer
defaults instead.

## What this change does

- Replaces the full message fetch from `RFC822` to `BODY.PEEK[]`
- Reads the returned message content from `BODY[]`, which is how
`net-imap` exposes the response attribute
- Keeps the existing `BODY.PEEK[HEADER]` header-fetch behavior unchanged
- Applies `SMTP_OPEN_TIMEOUT` and `SMTP_READ_TIMEOUT` to custom SMTP
email-channel replies
- Defaults custom SMTP reply delivery to `open_timeout: 15` and
`read_timeout: 30`
- Updates IMAP service specs for standard and Microsoft IMAP fetch flows
- Updates mailer specs for custom SMTP timeout settings

`BODY.PEEK[]` is preferable here because it fetches the full message
content without marking messages as read.

## Validation

- Configured a local email inbox against iCloud IMAP and SMTP
- Confirmed `FETCH RFC822` reproduces `Net::IMAP::ResponseParseError:
unexpected RPAR (expected ATOM or NIL)`
- Confirmed `BODY[]` and `BODY.PEEK[]` fetch the same mailbox
successfully
- Confirmed Chatwoot imports iCloud messages after the IMAP change
- Sent two outbound replies from the Chatwoot UI through iCloud SMTP
after applying the timeout settings
- Confirmed both UI-created outbound messages were marked `sent`, had
iCloud SMTP `source_id` values, and had no `external_error`
- Ran `bundle exec rspec spec/services/imap/fetch_email_service_spec.rb
spec/services/imap/microsoft_fetch_email_service_spec.rb`
- Ran `bundle exec rspec spec/mailers/conversation_reply_mailer_spec.rb`
2026-06-03 15:56:54 +05:30
1beaa284c6 feat: inline images in website and email channels (#14516)
# Pull Request Template

## Description

This PR adds support for inline image uploads in the reply editor for
Email and Website (chat widget) channels.

Agents can now insert images inline between text and resize them
directly in the editor by dragging the bottom corner, similar to the
help center editor experience.

Image sizes are preserved through markdown using the `cw_image_width`
URL param and render correctly in both outgoing emails and chat widget
messages.

Agents can also paste copied images directly into Email or Website
replies using **Shift+Cmd+V** (Shift+Ctrl+V on Windows/Linux). The image
gets inserted inline at the cursor position and supports resizing just
like uploaded images. Regular **Cmd+V / Ctrl+V** behavior remains
unchanged and continues to add images as attachments, so both inline and
attachment flows are supported.


### Prosemirror repo PR:
https://github.com/chatwoot/prosemirror-schema/pull/48

Fixes
https://linear.app/chatwoot/issue/CW-7133/inline-images-in-live-chat-and-email

https://linear.app/chatwoot/issue/CW-7225/ghsa-8j9w-jppp-xcfc-html-attribute-injection-via-unvalidated-cw-image

## Type of change

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

## How Has This Been Tested?

### Screencast



https://github.com/user-attachments/assets/a928f852-ab15-413a-9d35-6ea69b718ecf

<img width="414" height="654" alt="image"
src="https://github.com/user-attachments/assets/205e0729-8f2d-4cc5-9c55-7696f032eca4"
/>



## 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
- [ ] 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: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-03 15:05:17 +05:30
Sivin VargheseandGitHub d028cc1984 fix: prevent list marker overflow in messages (#14618) 2026-06-03 14:18:48 +05:30
Sivin VargheseandGitHub 0d59fb4459 fix: validate portal color format (#14632) 2026-06-03 14:18:19 +05:30
7acbe8b3ff fix(whatsapp): truncate location fallback_title to 255 chars to avoid silent message drop (#14517)
## Summary

`Whatsapp::IncomingMessageBaseService#attach_location` builds a
`fallback_title` by concatenating `location['name']` and
`location['address']` with no length cap, then stores it directly into
`Attachment#fallback_title`. `ApplicationRecord` enforces a generic
255-character limit on string columns, so any WhatsApp location whose
`"#{name}, #{address}"` exceeds 255 chars (a common case for Google
Places that include a long full address) raises
`ActiveRecord::RecordInvalid` deep inside the Sidekiq job. The message
and attachment INSERTs are part of the same transaction, so the whole
thing rolls back. Sidekiq retries once; the retry dedup-skips the wamid
silently and exits without an error. **Result: the message is
irrecoverably lost — no row in `messages`, no entry in the UI, no
outgoing webhook, no clue for the operator.**

Confirmed in `v4.13.0`, `v4.14.0`, and `develop` (commit `f33e469`,
2026-05-20). No upstream issue found before opening this PR.

## How to reproduce

1. From WhatsApp, share a Google Place whose `name + ", " + address` is
> 255 chars. The Spanish business address `Gremi de Fusters, 33,
Edificio VIP Asima, Piso 2, Local 2, Norte, 07009 Polígon industrial de
Son Castelló, Illes Balears, España` (132 chars) used as both `name` and
`address` is enough.
2. Sidekiq logs:
   ```
   ERROR ActiveRecord::RecordInvalid: Validation failed:
   Attachments fallback title is too long (maximum is 255 characters)
   ```
3. The `messages` table has no row. The conversation UI shows nothing
for that timestamp.
4. The first retry "Performed" successfully but creates nothing — the
dedup-by-source-id silently swallows the failure.

## Fix

Cap the existing concatenated title at 255 chars via `.first(255)`.
Minimal change, no behavioural difference for any message shorter than
the limit, prevents the silent data loss for any longer ones.

```diff
-    location_name = location['name'] ? "#{location['name']}, #{location['address']}" : ''
+    location_name = (location['name'] ? "#{location['name']}, #{location['address']}" : '').first(255)
```

## Alternatives considered

- **Increase the validation limit on `Attachment#fallback_title`**: more
invasive; would touch other inbound channels and possibly require a DB
column change.
- **Use `name` alone (no concat)**: cleaner semantically (in many real
payloads `name == address`), but changes user-visible behaviour. Left as
a follow-up if desired.
- **Truncate with ellipsis**: cosmetic only; deferred.

This PR is intentionally minimal so it can be merged on its own.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-03 12:50:21 +05:30
Vinícius FitznerandGitHub b791d75b30 fix(microsoft): prevent OAuth admin consent loop (#13962)
Fixes #9775

## Description

This fixes a repeated admin consent loop in the Microsoft OAuth flow
when connecting a Microsoft email inbox.

Chatwoot was always sending `prompt=consent` in the Microsoft
authorization URL. In the current code path, this parameter is only used
when building the authorization URL and is not required by the callback,
token exchange, token persistence, or refresh flow.

By removing the forced consent prompt, the OAuth flow can proceed
normally without repeatedly sending users back through the admin consent
screen.

## What changed

- removed `prompt: 'consent'` from the Microsoft authorization URL
- added a regression assertion to ensure `prompt` is not included in the
generated URL

## Why this is safe

- `redirect_uri`, `scope`, and `state` remain unchanged
- callback and token exchange flow remain unchanged
- refresh token flow remains unchanged
- no other part of the current Microsoft inbox flow depends on forcing a
consent screen

## Testing

- updated controller spec to assert that the generated authorization URL
does not include `prompt`
2026-06-03 12:05:25 +05:30