# 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>
124 lines
4.8 KiB
Ruby
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
|