123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class EmailHelper
- class Probe
- #
- # Try to guess email channel configuration from basic user data.
- #
- def self.full(params)
- user, domain = EmailHelper.parse_email(params[:email])
- if !user || !domain
- return {
- result: 'invalid',
- messages: {
- email: "Invalid email '#{params[:email]}'."
- },
- }
- end
- # probe provider based settings
- provider_map = EmailHelper.provider(params[:email], params[:password])
- domains = [domain]
- # get mx records, try to find provider based on mx records
- mx_records = EmailHelper.mx_records(domain)
- domains.concat(mx_records)
- provider_map.each_value do |settings|
- domains.each do |domain_to_check|
- next if !domain_to_check.match?(%r{#{settings[:domain]}}i)
- # add folder to config if needed
- if params[:folder].present? && settings[:inbound] && settings[:inbound][:options]
- settings[:inbound][:options][:folder] = params[:folder]
- end
- config_set_verify_ssl(settings[:inbound], params)
- config_set_verify_ssl(settings[:outbound], params)
- # probe inbound
- Rails.logger.debug { "INBOUND PROBE PROVIDER: #{settings[:inbound].inspect}" }
- result_inbound = EmailHelper::Probe.inbound(settings[:inbound])
- Rails.logger.debug { "INBOUND RESULT PROVIDER: #{result_inbound.inspect}" }
- next if result_inbound[:result] != 'ok'
- # probe outbound
- Rails.logger.debug { "OUTBOUND PROBE PROVIDER: #{settings[:outbound].inspect}" }
- result_outbound = EmailHelper::Probe.outbound(settings[:outbound], params[:email])
- Rails.logger.debug { "OUTBOUND RESULT PROVIDER: #{result_outbound.inspect}" }
- next if result_outbound[:result] != 'ok'
- return {
- result: 'ok',
- content_messages: result_inbound[:content_messages],
- archive_possible: result_inbound[:archive_possible],
- archive_week_range: result_inbound[:archive_week_range],
- setting: settings,
- }
- end
- end
- # probe guess settings
- # probe inbound
- inbound_mx = EmailHelper.provider_inbound_mx(user, params[:email], params[:password], mx_records)
- inbound_guess = EmailHelper.provider_inbound_guess(user, params[:email], params[:password], domain)
- inbound_map = inbound_mx + inbound_guess
- result = {
- result: 'ok',
- setting: {}
- }
- success = false
- inbound_map.each do |config|
- # add folder to config if needed
- if params[:folder].present? && config[:options]
- config[:options][:folder] = params[:folder]
- end
- # Add SSL verification flag to configuration, if needed.
- config_set_verify_ssl(config, params)
- Rails.logger.debug { "INBOUND PROBE GUESS: #{config.inspect}" }
- result_inbound = EmailHelper::Probe.inbound(config)
- Rails.logger.debug { "INBOUND RESULT GUESS: #{result_inbound.inspect}" }
- next if result_inbound[:result] != 'ok'
- success = true
- result[:setting][:inbound] = config
- result[:content_messages] = result_inbound[:content_messages]
- result[:archive_possible] = result_inbound[:archive_possible]
- result[:archive_week_range] = result_inbound[:archive_week_range]
- break
- end
- # give up, no possible inbound found
- if !success
- return {
- result: 'failed',
- reason: 'inbound failed',
- }
- end
- # probe outbound
- outbound_mx = EmailHelper.provider_outbound_mx(user, params[:email], params[:password], mx_records)
- outbound_guess = EmailHelper.provider_outbound_guess(user, params[:email], params[:password], domain)
- outbound_map = outbound_mx + outbound_guess
- success = false
- outbound_map.each do |config|
- # Add SSL verification flag to configuration, if needed.
- config_set_verify_ssl(config, params)
- Rails.logger.debug { "OUTBOUND PROBE GUESS: #{config.inspect}" }
- result_outbound = EmailHelper::Probe.outbound(config, params[:email])
- Rails.logger.debug { "OUTBOUND RESULT GUESS: #{result_outbound.inspect}" }
- next if result_outbound[:result] != 'ok'
- success = true
- result[:setting][:outbound] = config
- break
- end
- # give up, no possible outbound found
- if !success
- return {
- result: 'failed',
- reason: 'outbound failed',
- }
- end
- Rails.logger.debug { "PROBE FULL SUCCESS: #{result.inspect}" }
- result
- end
- #
- # Validate an inbound email channel configuration.
- #
- def self.inbound(params)
- adapter = params[:adapter].downcase
- # validate adapter
- if !EmailHelper.available_driver[:inbound][adapter.to_sym]
- return {
- result: 'failed',
- message: "Unknown adapter '#{adapter}'",
- }
- end
- # connection test
- result_inbound = {}
- begin
- driver_class = "Channel::Driver::#{adapter.to_classname}".constantize
- driver_instance = driver_class.new
- result_inbound = driver_instance.fetch(params[:options], nil, 'check')
- rescue => e
- Rails.logger.debug { e }
- return {
- result: 'invalid',
- settings: params,
- message: e.message,
- message_human: translation(e.message),
- invalid_field: invalid_field(e.message),
- }
- end
- result_inbound
- end
- #
- # Validate an outbound email channel configuration.
- #
- def self.outbound(params, email, subject = nil)
- adapter = params[:adapter].downcase
- # validate adapter
- if !EmailHelper.available_driver[:outbound][adapter.to_sym]
- return {
- result: 'failed',
- message: "Unknown adapter '#{adapter}'",
- }
- end
- # prepare test email
- # rubocop:disable Zammad/DetectTranslatableString
- mail = if subject
- {
- from: email,
- to: email,
- subject: "Zammad Getting started Test Email #{subject}",
- body: "This is a test email from Zammad to check if email sending and receiving work correctly.\n\nYou can ignore or delete this email.",
- }
- else
- {
- from: email,
- to: 'verify-external-smtp-sending@discard.zammad.org',
- subject: 'This is a Test Email',
- body: "This is a test email from Zammad to verify if Zammad can send emails to an external address.\n\nIf you see this email, you can ignore or delete it.",
- }
- end
- # rubocop:enable Zammad/DetectTranslatableString
- if subject.present?
- mail['X-Zammad-Test-Message'] = subject
- end
- mail['X-Zammad-Ignore'] = 'true'
- mail['X-Zammad-Fqdn'] = Setting.get('fqdn')
- mail['X-Zammad-Verify'] = 'true'
- mail['X-Zammad-Verify-Time'] = Time.zone.now.iso8601
- mail['X-Loop'] = 'yes'
- mail['Precedence'] = 'bulk'
- mail['Auto-Submitted'] = 'auto-generated'
- mail['X-Auto-Response-Suppress'] = 'All'
- # test connection
- begin
- driver_class = "Channel::Driver::#{adapter.to_classname}".constantize
- driver_instance = driver_class.new
- driver_instance.deliver(
- params[:options],
- mail,
- )
- rescue => e
- Rails.logger.debug { e }
- # check if sending email was ok, but mailserver rejected
- if !subject
- white_map = {
- 'Recipient address rejected' => true,
- 'Sender address rejected: Domain not found' => true,
- }
- white_map.each_key do |key|
- next if !e.message.match?(%r{#{Regexp.escape(key)}}i)
- return {
- result: 'ok',
- settings: params,
- notice: e.message,
- }
- end
- end
- return {
- result: 'invalid',
- settings: params,
- message: e.message,
- message_human: translation(e.message),
- invalid_field: invalid_field(e.message),
- }
- end
- {
- result: 'ok',
- }
- end
- def self.invalid_field(message_backend)
- invalid_fields.each do |key, fields|
- return fields if message_backend.match?(%r{#{Regexp.escape(key)}}i)
- end
- {}
- end
- def self.invalid_fields
- {
- 'authentication failed' => { user: true, password: true },
- 'Username and Password not accepted' => { user: true, password: true },
- 'Incorrect username' => { user: true, password: true },
- 'Lookup failed' => { user: true },
- 'Invalid credentials' => { user: true, password: true },
- 'getaddrinfo: nodename nor servname provided, or not known' => { host: true },
- 'getaddrinfo: Name or service not known' => { host: true },
- 'No route to host' => { host: true },
- 'execution expired' => { host: true },
- 'Connection refused' => { host: true },
- 'Mailbox doesn\'t exist' => { folder: true },
- 'Folder doesn\'t exist' => { folder: true },
- 'Unknown Mailbox' => { folder: true },
- }
- end
- def self.translation(message_backend)
- translations.each do |key, message_human|
- return message_human if message_backend.match?(%r{#{Regexp.escape(key)}}i)
- end
- nil
- end
- def self.translations
- {
- 'authentication failed' => __('Authentication failed.'),
- 'Username and Password not accepted' => __('Authentication failed.'),
- 'Incorrect username' => __('Authentication failed due to incorrect username.'),
- 'Lookup failed' => __('Authentication failed due to incorrect username.'),
- 'Invalid credentials' => __('Authentication failed due to incorrect credentials.'),
- 'authentication not enabled' => __('Authentication not possible (not offered by the service)'),
- 'getaddrinfo: nodename nor servname provided, or not known' => __('The hostname could not be found.'),
- 'getaddrinfo: Name or service not known' => __('The hostname could not be found.'),
- 'No route to host' => __('There is no route to this host.'),
- 'execution expired' => __('This host cannot be reached.'),
- 'Connection refused' => __('The connection was refused.'),
- }
- end
- def self.config_set_verify_ssl(config, params)
- return if !config[:options]
- if params.key?(:ssl_verify)
- config[:options][:ssl_verify] = params[:ssl_verify]
- elsif config[:options][:ssl] || config[:options][:start_tls]
- config[:options][:ssl_verify] = true
- else
- config[:options][:ssl_verify] ||= false
- end
- end
- end
- end
|