Files
chatwoot/app/helpers/api/v1/inboxes_helper.rb
202403873d feat: Ability to specify the authentication type for imap server (#12306)
# Pull Request Template

## Description

This PR adds IMAP authentication mechanism selection to Chatwoot's email
inbox configuration. Users can now choose between 'plain', 'login', and
'cram-md5' authentication methods when configuring IMAP settings,
providing flexibility for different email providers that require
specific authentication types.

https://github.com/chatwoot/chatwoot/issues/8867

The implementation includes:
- Frontend dropdown with numeric keys (1, 2, 3) matching SMTP auth style
- Backend API validation for allowed authentication mechanisms
- Consistent 'cram-md5' format throughout the codebase
- Updated IMAP service to handle different auth types properly

This feature maintains consistency with existing SMTP authentication
options and follows the established UI/UX patterns in the application.

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] 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?

### Manual Testing:
- Tested in Docker environment
- Verified IMAP auth dropdown appears in inbox settings
- Confirmed all three auth mechanisms (plain, login, cram-md5) can be
selected and saved
- Tested API validation by attempting to save invalid auth mechanisms

### Automated Testing:
- Updated existing IMAP service tests to use consistent lowercase values
- Updated API controller tests for authentication parameter handling
- All tests pass locally with the new changes

### Test Configuration:
- Tested with both new and existing inbox configurations

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

## Additional Notes

- This feature is backward compatible and doesn't break existing IMAP
configurations
- The 'cram-md5' format is used consistently throughout (UI, API,
storage, services)
- Net::IMAP compatibility is maintained by converting to 'CRAM-MD5'
internally
- Follows the same pattern established by SMTP authentication
configuration

---------

Co-authored-by: João Santos <joao.santos@madigital.eu>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-05-08 16:40:15 +05:30

124 lines
4.8 KiB
Ruby

module Api::V1::InboxesHelper
def inbox_name(channel)
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
permitted_params[:name]
end
def validate_email_channel(attributes)
channel_data = permitted_params(attributes)[:channel]
validate_imap(channel_data)
validate_smtp(channel_data)
end
private
def validate_imap(channel_data)
return unless channel_data.key?('imap_enabled') && channel_data[:imap_enabled]
# Validate the user-selected auth mechanism before opening the connection.
authentication = Imap::Authentication.validate_user_configurable!(channel_data[:imap_authentication])
# Use the same auth adapter as the fetch service so LOGIN uses the IMAP LOGIN command,
# not SASL AUTH=LOGIN.
check_imap_connection(channel_data, authentication)
end
def validate_smtp(channel_data)
return unless channel_data.key?('smtp_enabled') && channel_data[:smtp_enabled]
smtp = Net::SMTP.new(channel_data[:smtp_address], channel_data[:smtp_port])
set_smtp_encryption(channel_data, smtp)
check_smtp_connection(channel_data, smtp)
end
def check_imap_connection(channel_data, authentication)
imap = open_imap_connection(channel_data, authentication)
rescue SocketError => e
raise StandardError, I18n.t('errors.inboxes.imap.socket_error')
rescue Net::IMAP::NoResponseError => e
raise StandardError, I18n.t('errors.inboxes.imap.no_response_error')
rescue Errno::EHOSTUNREACH => e
raise StandardError, I18n.t('errors.inboxes.imap.host_unreachable_error')
rescue Net::OpenTimeout => e
raise StandardError,
I18n.t('errors.inboxes.imap.connection_timed_out_error', address: channel_data[:imap_address], port: channel_data[:imap_port])
rescue Net::IMAP::Error => e
raise StandardError, I18n.t('errors.inboxes.imap.connection_closed_error')
rescue StandardError => e
raise StandardError, e.message
ensure
imap.disconnect if imap.present? && !imap.disconnected?
Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present?
end
def open_imap_connection(channel_data, authentication)
imap = build_imap_connection(channel_data)
Imap::Authentication.authenticate!(imap, authentication, channel_data[:imap_login], channel_data[:imap_password])
imap
end
def build_imap_connection(channel_data)
Net::IMAP.new(channel_data[:imap_address], port: channel_data[:imap_port], ssl: channel_data[:imap_enable_ssl])
end
def check_smtp_connection(channel_data, smtp)
smtp.open_timeout = 10
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
channel_data[:smtp_authentication]&.to_sym || :login)
smtp.finish
rescue Net::SMTPAuthenticationError
raise StandardError, I18n.t('errors.inboxes.smtp.authentication_error')
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Net::OpenTimeout
raise StandardError, I18n.t('errors.inboxes.smtp.connection_error')
rescue OpenSSL::SSL::SSLError
raise StandardError, I18n.t('errors.inboxes.smtp.ssl_error')
rescue Net::SMTPServerBusy, Net::SMTPSyntaxError, Net::SMTPFatalError
raise StandardError, I18n.t('errors.inboxes.smtp.smtp_error')
rescue StandardError => e
raise StandardError, e.message
end
def set_smtp_encryption(channel_data, smtp)
if channel_data[:smtp_enable_ssl_tls]
set_smtp_ssl_method(smtp, :enable_tls, channel_data[:smtp_openssl_verify_mode])
elsif channel_data[:smtp_enable_starttls_auto]
set_smtp_ssl_method(smtp, :enable_starttls_auto, channel_data[:smtp_openssl_verify_mode])
end
end
def set_smtp_ssl_method(smtp, method, openssl_verify_mode)
return unless smtp.respond_to?(method)
context = enable_openssl_mode(openssl_verify_mode) if openssl_verify_mode
context ? smtp.send(method, context) : smtp.send(method)
end
def enable_openssl_mode(smtp_openssl_verify_mode)
openssl_verify_mode = "OpenSSL::SSL::VERIFY_#{smtp_openssl_verify_mode.upcase}".constantize if smtp_openssl_verify_mode.is_a?(String)
context = Net::SMTP.default_ssl_context
context.verify_mode = openssl_verify_mode
context
end
def account_channels_method
{
'web_widget' => Current.account.web_widgets,
'api' => Current.account.api_channels,
'email' => Current.account.email_channels,
'line' => Current.account.line_channels,
'telegram' => Current.account.telegram_channels,
'whatsapp' => Current.account.whatsapp_channels,
'sms' => Current.account.sms_channels
}[permitted_params[:channel][:type]]
end
def validate_limit
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
end
end