## Description
Make conversation unread counts always available at runtime by removing
account feature checks from the API endpoint, unread-count listener,
notifier, and ActionCable broadcast path.
Update the dashboard to fetch sidebar unread counts for the active
account without checking FEATURE_FLAGS.CONVERSATION_UNREAD_COUNTS, and
remove the now-unused store clear action that only supported the
disabled state.
Keep the feature entry in config/features.yml to preserve flag bit
order, but mark it enabled and deprecated so fresh installs default to
the always-on behavior while feature-management UI hides it.
Leave existing installation default rows untouched; no migration is
included, so upgraded installs may still store the old flag value but
runtime behavior no longer depends on it.
Update specs around the new always-on contract and remove obsolete
disabled-feature assertions.
Fixes # CW-7237
## 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?
Update specs around the new always-on contract and remove obsolete
disabled-feature assertions. Ran specs locally for the changes.
## 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
Updates the Create New Message API documentation to explain how to send
file attachments with multipart form data.
Closes#10472Closes#12672
## Why
The endpoint already accepts attachment uploads, but the published API
docs only described the JSON request body. That made it unclear that
clients need to use multipart form data and send files through the
`attachments[]` field.
## What changed
- Adds `multipart/form-data` as a documented request body option for
Create New Message.
- Documents the `attachments` binary array and form encoding used by
`attachments[]`.
- Adds a multipart cURL request example for a message with an
attachment.
- Regenerates the Swagger JSON artifacts.
## Screenshots
Create New Message API docs with the multipart cURL sample and
`multipart/form-data` request sample selected:
<img width="1702" height="1083"
alt="create-message-attachment-docs-multipart"
src="https://github.com/user-attachments/assets/5fef9082-6eb9-494d-91d1-8dc0b7880212"
/>
## How to test
1. Open `/swagger`.
2. Navigate to Application -> Messages -> Create New Message.
3. Confirm the endpoint documents both `application/json` and
`multipart/form-data`, including the attachment payload schema.
When connecting a Gmail or Outlook inbox during onboarding, the OAuth
flow used to drop users in inbox settings, breaking onboarding. The
OAuth start endpoint now accepts an optional `return_to=onboarding`
hint, carried tamper-proof inside the signed `state`, and the callback
uses it to return the user to the onboarding inbox-setup screen. Without
the hint, behavior is unchanged.
This is the backend half only; the frontend that sends
`return_to=onboarding` ships separately.
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Outgoing email on a Microsoft email inbox was failing with `535 5.7.3
Authentication unsuccessful` immediately after the user
re-authenticated, while incoming (IMAP) continued to work. The root
cause is in our OAuth callback: we persist the id_token's `email` claim
into `channel.imap_login`, but Microsoft's SMTP AUTH (XOAUTH2) validates
the username against the access token's **UPN**, not the mailbox's
primary SMTP address or aliases. When those diverge — common in tenants
that use one domain for sign-in identities and another for mailbox
addresses — SMTP rejects every send.
This PR makes `OauthCallbackController#update_channel` prefer
`preferred_username` (v2.0) / `upn` (v1.0) from the id_token over
`email`, with `email` as the fallback so Google flows are unchanged.
## What changed
- `app/controllers/oauth_callback_controller.rb` — extract
`imap_login_identity` (default: `users_data['email']`, same as before).
`update_channel` now calls it instead of inlining the email claim. No
behavioural change in the base controller.
- `app/controllers/microsoft/callbacks_controller.rb` — override
`imap_login_identity` to return `preferred_username || upn || super`.
Provider-specific knowledge stays in the provider subclass; Google's
flow is literally untouched.
- `spec/controllers/microsoft/callbacks_controller_spec.rb` — adds one
example that reproduces the divergent shape (`email: <alias>`,
`preferred_username: <upn>`) and asserts `imap_login` lands on the UPN
while `channel.email` stays on the mailbox alias.
`channel.email` and `find_channel_by_email` still key on the id_token's
`email` claim everywhere, so customer-facing From identity and
reconnect-by-email matching are unchanged.
Behavioural matrix:
| Provider / shape | `imap_login` before | `imap_login` after |
|-----------------------------------------------------------------|---------------------|--------------------|
| Microsoft, `upn == email` (common case) | email | UPN (= email, same
string) |
| Microsoft, `upn != email` (e.g. UPN on one domain, mailbox alias on
another) | alias (broken) | UPN (works) |
| Google | email | email (no change, base impl used) |
## Why this happens (Microsoft side, for context)
Two facts that together produce the bug — one is documented, the other
we verified empirically because Microsoft's docs don't address it:
1. **`email` and `upn` are different claims and can legitimately
diverge.** In Entra, the UPN is the sign-in identity; the id_token's
`email` claim is the user's mailbox property (which can be a proxy
address). For v2.0 tokens (which is what we use —
`/common/oauth2/v2.0/token` in `MicrosoftConcern`), the documented
"username to sign in as" claim is `preferred_username`. See [ID token
claims
reference](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference).
So `users_data['email']` was the wrong source for `imap_login`; tenants
where UPN == primary SMTP happened to mask the bug.
2. **Exchange's XOAUTH2 SMTP rejects aliases in the `user=` field, even
though IMAP accepts them.** This asymmetry is **not** in [Microsoft's
canonical XOAUTH2
doc](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth)
— the doc treats the SASL `user=` field uniformly across IMAP, POP, and
SMTP, with only a shared-mailbox carve-out spelled out. We confirmed the
SMTP-strict behaviour empirically by running an XOAUTH2 AUTH probe with
the same access token: `user=<alias>` returned `535 5.7.3`, `user=<UPN>`
returned `235`. Same token, only the `user=` field differed. That's what
tied the symptom to claim-shape.
Hence the patch: read the documented "sign-in name" claim and persist
that, not the customer-facing mailbox address.
## How to reproduce (before the fix)
1. Set up a Microsoft email inbox in a tenant where the user's UPN
domain differs from the user's primary SMTP / mailbox domain (e.g. UPN
`user@tenant-a.example`, mailbox `User@tenant-b.example` where
`tenant-b.example` is a proxy address on the same mailbox).
2. Connect or re-authenticate the inbox through the standard flow.
3. Inspect the channel:
```ruby
ch.imap_login # => "User@tenant-b.example" (alias — wrong)
token = ch.provider_config['access_token']
JSON.parse(Base64.urlsafe_decode64(token.split('.')[1].then { |s| s +
'=' * (-s.length % 4) }))['upn']
# => "user@tenant-a.example" (UPN — what SMTP actually needs)
```
4. Send a reply. SMTP returns `535 5.7.3 Authentication unsuccessful`.
Incoming IMAP continues to work fine.
After the fix, `imap_login` is set to the UPN at callback time and SMTP
succeeds.
## Notes for review
- The id_token decoding path (`users_data`) already exists and is
trusted by the rest of the callback — no new attack surface.
- Scoped to Microsoft on purpose. A provider-agnostic fallback chain in
the base controller would also have worked (Google id_tokens don't carry
`preferred_username` / `upn`, so it'd land on email anyway), but keeping
Google's code path identical to today removes any risk of an unintended
interaction with whatever a future Google update might add to its
id_token. Pattern matches the existing `find_channel_by_email` override
Google already has.
- The [id_token claims
reference](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference)
warns that `preferred_username` is mutable and "can't be used to make
authorization decisions." That warning targets apps using the claim as a
stable cross-session identifier for app-level authz — we're not. We use
it as the SASL `user=` string for SMTP AUTH, validated by Microsoft
against the same token it just issued. The claim's mutability matters
only if a tenant admin renames a UPN between re-auths, in which case the
stored `imap_login` goes stale and SMTP 535s — but the pre-patch code
has the identical mutability characteristic on the `email` claim (rename
a mailbox's primary SMTP and you get the same stale-then-535). The patch
doesn't enlarge that failure surface; it shrinks it, because
UPN-vs-alias divergence is now handled rather than always-broken.
- Related but out of scope: SMTP failures don't trigger
`channel.authorization_error!` today. `ExceptionList::SMTP_EXCEPTIONS`
(`lib/exception_list.rb`) only contains `Net::SMTPSyntaxError`;
`Net::SMTPAuthenticationError` bubbles unhandled, and
`ApplicationMailer#handle_smtp_exceptions` only logs. So a 535 — whether
from this bug or any other identity drift — never prompts re-auth in the
UI, unlike the IMAP path (`fetch_imap_emails_job` catches
`OAuth2::Error` and calls `authorization_error!`). Worth a separate PR;
flagging here so we don't pretend stale-identity SMTP errors are
self-healing.
- The alternative I considered — decoding the access token's `upn`
directly — is more correct in principle but relies on Microsoft access
tokens being JWTs, which is undocumented behaviour for the
`outlook.office.com` audience and contradicts the [docs
guidance](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens)
that access tokens are opaque to clients.
- **Not addressed here, flagging for follow-up:** existing channels that
were re-authenticated before this fix still hold the alias in
`imap_login` and will keep 535ing until the user re-auths again. A
one-shot rake task could decode the stored access tokens and reconcile,
but fixing forward via natural re-auth is lower-risk; let me know if you
want the backfill.
- Also out of scope: `app/mailers/conversation_reply_mailer_helper.rb`
reads `provider_config['access_token']` raw without going through
`Microsoft::RefreshOauthTokenService`, while the IMAP path refreshes.
That's a real asymmetry but unrelated to this bug (the token here was
minutes-old) and worth its own PR.
When connecting a TikTok inbox during onboarding, the OAuth flow used to
drop users in inbox settings, breaking onboarding. The OAuth start
endpoint now accepts an optional `return_to=onboarding` hint, carried
tamper-proof inside the signed `state` (a claim on TikTok's signed JWT),
and the callback uses it to return the user to the onboarding
inbox-setup screen. Without the hint, behavior is unchanged.
This is the backend half only; the frontend that sends
`return_to=onboarding` ships separately.
## What changed
- `Tiktok::IntegrationHelper`: the signed JWT carries an optional
`return_to` claim, added only when present (a request without it is
byte-identical to before); added `tiktok_token_return_to` to read it;
`decode_token` now returns the full payload and `verify_tiktok_token`
derives the account id from it.
- `Tiktok::AuthorizationsController#create` passes `params[:return_to]`
into the token.
- `Tiktok::CallbacksController` redirects to the onboarding inbox-setup
screen when `return_to == 'onboarding'`, before the normal
settings/agents redirect.
- Added the `app_onboarding_inbox_setup` route (shared with the sibling
Gmail/Outlook and Instagram PRs — keep a single copy on merge to avoid a
duplicate route name).
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Fixes Facebook fallback and shared-post attachments so they render as
clickable links in conversations.
Closes:
- https://github.com/chatwoot/chatwoot/issues/4767
- https://github.com/chatwoot/chatwoot/issues/5327
Why:
Facebook can send shared links as `fallback` attachments with a
top-level `url`, and shared posts as `share` attachments with the URL
under `payload.url`. The current flow either misses the nested URL or
treats `share` as downloadable media, so these messages do not render
correctly.
What changed:
- Store Facebook fallback URLs from either `attachment.url` or
`attachment.payload.url`.
- Treat Facebook `share` attachments as fallback link attachments
instead of downloading them as files.
- Render fallback attachments in the next message bubble UI as clickable
links.
How to test:
1. Connect a Facebook inbox.
2. Send a shared link to the page.
3. Send/share a Facebook post to the page.
4. Open the conversation in Chatwoot.
5. Confirm both messages appear as clickable link bubbles.
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Summary
One-off SMS and WhatsApp campaigns now show a `Processing` state while
the audience send is in progress. The campaign moves to `Completed`
after processing finishes, and already-processing campaigns are skipped
by the scheduler to avoid duplicate sends.
## Closes
- [CW-6037: feat: Introduce an in-progress status for
campaigns](https://linear.app/chatwoot/issue/CW-6037/feat-introduce-an-in-progress-status-for-campaigns)
## Screenshot
SMS campaign card showing the new `Processing` status.
<img width="3840" height="2160" alt="framed-campaign-processing-status"
src="https://github.com/user-attachments/assets/de7913b5-65fb-4121-9034-24a568eb0382"
/>
## What changed
- Added `processing` as a campaign status.
- Mark one-off campaigns as `processing` under a row lock before the
send service runs.
- Complete SMS, Twilio SMS, and WhatsApp one-off campaigns after
audience processing finishes.
- Keep campaigns in `processing` if an unexpected service error escapes,
so the scheduler does not automatically resend the audience.
- Added the `Processing` label for SMS and WhatsApp campaign cards.
## Known operational behavior
If a worker is interrupted or an unexpected service error escapes after
a campaign is marked `processing`, the campaign can remain in
`processing`. This is intentional for now to avoid automatic
full-audience resends. Installation admins can decide whether to mark
the campaign completed or restart it manually from the Rails console
after checking what was sent.
## How to test
- Create a one-off SMS or WhatsApp campaign scheduled for now.
- Run the scheduled job or trigger the campaign job.
- Confirm the campaign card shows `Processing` while the audience is
being processed. For small audiences, refresh during processing or use a
larger audience so the state is observable.
- Confirm the campaign moves to `Completed` after audience processing
finishes.
- Confirm an already-processing campaign is not enqueued again by the
scheduled job.
Documents the existing API inbox webhook_url request field and expands
inbox create/update request docs with channel-specific schemas and
examples.
Closes#14591
## Why
API inboxes already accept channel.webhook_url through Channel::Api
editable attributes, but the OpenAPI request schemas did not document
the field. The previous mixed channel object also made it hard to see
which fields belong to each supported channel type.
## What changed
- Added named channel payload schemas for supported inbox create channel
types.
- Added channel-specific create request samples in this order: Website
inbox, API channel, Email channel, LINE channel, Telegram channel,
WhatsApp channel, SMS channel.
- Added common inbox fields such as greeting_enabled, greeting_message,
enable_auto_assignment, working_hours_enabled, and timezone to request
samples.
- Kept website-only flags such as allow_messages_after_resolved and
enable_email_collect only in website inbox samples.
- Annotated inbox request field descriptions with channel availability
where Swagger UI cannot dynamically filter fields.
- Added channel-specific update settings schemas and request samples for
editable channel attributes.
- Documented channel.webhook_url for API inbox create/update payloads.
- Rebuilt generated Swagger JSON and tag-group specs.
## Screenshots
**Website inbox request sample**
Shows the request sample selector set to Website inbox, including
website-only fields such as enable_email_collect and
allow_messages_after_resolved.
<img width="3840" height="2160" alt="Website inbox request sample"
src="https://github.com/user-attachments/assets/ad130f04-b336-4cc3-aba1-e934b646d447"
/>
**API channel request sample**
Shows the selector switched to API channel, with API-specific fields
such as webhook_url, hmac_mandatory, and additional_attributes.
<img width="3840" height="2160" alt="API channel request sample"
src="https://github.com/user-attachments/assets/09484816-1d47-490f-9ab4-195835ed5cd3"
/>
**Expanded channel schema**
Shows the request-body schema with channel expanded, including the type
selector and channel-specific fields under it.
<img width="3840" height="2160" alt="Expanded channel schema"
src="https://github.com/user-attachments/assets/f6c4878e-ac34-40fe-8be3-5913f27cbdd8"
/>
## Validation
- bundle exec rake swagger:build
- bundle exec rspec spec/swagger/openapi_spec.rb
- Rendered the local Swagger page and verified the request sample
dropdown order and API channel payload.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Enable the Companies feature automatically for Chatwoot Cloud accounts
on the Business plan and higher.
## Closes
None.
## How to test
Upgrade or reconcile a Cloud account on Business or Enterprise and
verify Companies is available. Reconcile a Startups account and verify
Companies remains disabled.
## What changed
- Added companies to the Business plan feature set, which Enterprise
inherits through the existing hierarchy.
- Added billing specs that assert Companies is disabled for Startups and
enabled for Business and Enterprise.
Allows contact managers to export and import contacts from the Contacts
page while keeping plain agents blocked. The contacts action menu now
mirrors backend permissions for both export and import.
## Closes
- https://linear.app/chatwoot/issue/CW-4438/contact-export-is-broken
## What changed
- Allows Enterprise custom roles with `contact_manage` to pass
`ContactPolicy#export?` and `ContactPolicy#import?`.
- Shows Export and Import to admins and contact managers only.
- Adds Enterprise policy coverage for contact export and import.
## Screenshots
Admin: Export and Import are available.
<img width="3840" height="2160" alt="Admin contact actions with Export
and Import visible"
src="https://github.com/user-attachments/assets/2b2cdaf2-ca8f-470d-be34-31cba68b9dce"
/>
Contact manager: Export and Import are available.
<img width="3840" height="2160" alt="Contact manager contact actions
with Export and Import visible"
src="https://github.com/user-attachments/assets/48fc038b-2e78-4d0c-ba17-a5965641bd88"
/>
Regular agent: Export and Import are hidden.
<img width="3840" height="2160" alt="Regular agent contact actions with
Export and Import hidden"
src="https://github.com/user-attachments/assets/a63b5731-743a-4223-8dab-ce58383067fe"
/>
## How to test
- Sign in as an administrator and open Contacts; the action menu shows
Export and Import.
- Sign in as a custom-role user with `contact_manage`; the action menu
shows Export and Import.
- Sign in as a plain agent; Export and Import are not available and both
APIs remain unauthorized.
Depends on: https://github.com/chatwoot/chatwoot/pull/14370
This PR creates a new onboarding controller, this allows more control
that the default account update API. Allowing us to spin tasks and
update details required specifically during the onboarding flow
## Linear ticket
https://linear.app/chatwoot/issue/CW-7187/voice-calls-followup-tasks
## Description
Improvements to the WhatsApp voice-calling experience plus a cheaper,
more accurate audio-transcription model.
- First-time callers now get a real name. An inbound WhatsApp call
creates the contact from the caller's WhatsApp profile name instead of
the bare phone number.
- Clear, consistent call attribution. Call bubbles show a unified
"Handled by {agent}"
- Cleaner call widget. The dismiss (✕) button is shown only for incoming
calls
- WhatsApp calling for manual inboxes. voice_calling_supported? now
covers any whatsapp_cloud inbox
- Transcription: whisper-1 → gpt-4o-mini-transcribe.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## 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
# Pull Request Template
## Description
Better scheduling and queueing mechanics for document auto-sync
- add jitter plan wise for document sync
- move auto-sync documents to purgeable queue
## Type of change
Please delete options that are not relevant.
- [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 tested and with 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
---------
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
During account enrichment, WebsiteBrandingService now probes the signup
domain's MX records to infer whether email is hosted on Google Workspace
or Microsoft 365, adding `email_provider` (`google`/`microsoft`/`nil`)
to `brand_info`. Matching is anchored on a label boundary so lookalike
domains aren't misclassified, and lookup failures fall back to `nil`.
This lets downstream UI suggest the right mailbox integration (Gmail vs
Outlook).
This PR updates two dependencies — `faraday` (2.14.1 → 2.14.2) and `jwt`
(2.10.1 → 2.10.3) — to pick up security patches flagged by
`bundle-audit`. Both are bumped to the minimal patched release within
their existing major lines to keep the blast radius small.
### Faraday
`Faraday::Connection#build_exclusive_url` still allowed a
protocol-relative host override when the request target was passed as a
`URI` object (rather than a `String`), bypassing the earlier fix for the
string-based variant (CVE-2026-25765 / GHSA-33mh-2634-fwr2). On a
fixed-base connection this could redirect a request to an
attacker-controlled host while still forwarding connection-scoped
headers such as `Authorization` — i.e. off-host request forgery
(CVE-2026-33637 / GHSA-5rv5-xj5j-3484).
The fix is a clean patch bump to `2.14.2`, within Faraday's existing
version range — no API changes and no other gems affected.
### JWT
`jwt` 2.10.1 accepts an empty/`nil` HMAC key during verification:
`JWT.decode(token, "", true, algorithm: 'HS256')` (and keyfinder paths
returning `""`/`nil`) verify a forged token, because the empty-key HMAC
digest is treated as valid and `enforce_hmac_key_length` defaults to
`false` (CVE-2026-45363, High).
The advisory offers two fixes — `~> 2.10.3` or `>= 3.2.0`. We chose
**2.10.3** deliberately: jumping to 3.x cascaded into upgrading
`oauth2`, `twilio-ruby`, `googleauth`, `web-push`, and `signet` (all
pinned `jwt < 3.0`), and `jwt` is used directly in 8+ places here (token
services, OAuth callbacks, integration helpers), so a major bump carries
real breakage risk for no extra security benefit. The Gemfile is pinned
`'~> 2.10', '>= 2.10.3'` to hold the 2.x line.
**Spec changes.** 2.10.3 tightens key handling: HMAC sign/verify now
raises on a `nil`, empty, or non-`String` key instead of silently
coercing it. A few specs relied on the old lax behaviour and needed
updating:
- `microsoft` / `google` callback specs built unsigned ID tokens via
`JWT.encode(payload, false)`. Replaced with the correct unsigned form,
`JWT.encode(payload, nil, 'none')`.
- `instagram` / `linear` / `shopify` helper specs have a "client secret
not configured" context where `client_secret` is `nil`. Their shared
`valid_token` `let` signed with that `nil` secret, which Ruby evaluates
before the helper runs — now raising. Since the helper short-circuits on
the blank secret and never decodes the token, those contexts now
override `valid_token` with a throwaway string.
**Production is unaffected.** Every production HMAC path uses a real,
non-empty key — `Rails.application.secret_key_base` (`BaseTokenService`,
`Widget::TokenService`) or a client secret guarded by `return if
client_secret.blank?` (Instagram/TikTok/Shopify/Linear helpers). The one
`nil`-key call, `JWT.decode(id_token, nil, false)` in
`OauthCallbackController`, runs with verification disabled, so the key
is never inspected. Twilio voice tokens use `Twilio::JWT::AccessToken`
from `twilio-ruby`, not this gem. The specs failed precisely because
they exercised the unsafe empty-key pattern the patch now blocks —
production never did.
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.
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>
# 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