e552795183
(cherry picked from commit 3ec848a067218aa83f40d71e46c60234b3e5611b)
424 lines
13 KiB
Ruby
424 lines
13 KiB
Ruby
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
|
|
|
class EmailHelper
|
|
class Probe
|
|
|
|
=begin
|
|
|
|
get result of probe
|
|
|
|
result = EmailHelper::Probe.full(
|
|
email: 'znuny@example.com',
|
|
password: 'somepassword',
|
|
folder: 'some_folder', # optional im imap
|
|
)
|
|
|
|
returns on success
|
|
|
|
{
|
|
result: 'ok',
|
|
settings: {
|
|
inbound: {
|
|
adapter: 'imap',
|
|
options: {
|
|
host: 'imap.gmail.com',
|
|
port: 993,
|
|
ssl: true,
|
|
user: 'some@example.com',
|
|
password: 'password',
|
|
folder: 'some_folder', # optional im imap
|
|
},
|
|
},
|
|
outbound: {
|
|
adapter: 'smtp',
|
|
options: {
|
|
host: 'smtp.gmail.com',
|
|
port: 25,
|
|
ssl: true,
|
|
user: 'some@example.com',
|
|
password: 'password',
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
returns on fail
|
|
|
|
result = {
|
|
result: 'failed',
|
|
}
|
|
|
|
=end
|
|
|
|
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
|
|
|
|
# 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
|
|
|
|
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|
|
|
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
|
|
|
|
=begin
|
|
|
|
get result of inbound probe
|
|
|
|
result = EmailHelper::Probe.inbound(
|
|
adapter: 'imap',
|
|
options: {
|
|
host: 'imap.gmail.com',
|
|
port: 993,
|
|
ssl: true,
|
|
user: 'some@example.com',
|
|
password: 'password',
|
|
folder: 'some_folder', # optional
|
|
}
|
|
)
|
|
|
|
returns on success
|
|
|
|
{
|
|
result: 'ok'
|
|
}
|
|
|
|
returns on fail
|
|
|
|
result = {
|
|
result: 'invalid',
|
|
settings: {
|
|
host: 'imap.gmail.com',
|
|
port: 993,
|
|
ssl: true,
|
|
user: 'some@example.com',
|
|
password: 'password',
|
|
folder: 'some_folder', # optional im imap
|
|
},
|
|
message: 'error message from used lib',
|
|
message_human: 'translated error message, readable for humans',
|
|
}
|
|
|
|
=end
|
|
|
|
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
|
|
|
|
=begin
|
|
|
|
get result of outbound probe
|
|
|
|
result = EmailHelper::Probe.outbound(
|
|
{
|
|
adapter: 'smtp',
|
|
options: {
|
|
host: 'smtp.gmail.com',
|
|
port: 25,
|
|
ssl: true,
|
|
user: 'some@example.com',
|
|
password: 'password',
|
|
}
|
|
},
|
|
'sender_and_recipient_of_test_email@example.com',
|
|
'subject of probe email',
|
|
)
|
|
|
|
returns on success
|
|
|
|
{
|
|
result: 'ok'
|
|
}
|
|
|
|
returns on fail
|
|
|
|
result = {
|
|
result: 'invalid',
|
|
settings: {
|
|
host: 'stmp.gmail.com',
|
|
port: 25,
|
|
ssl: true,
|
|
user: 'some@example.com',
|
|
password: 'password',
|
|
},
|
|
message: 'error message from used lib',
|
|
message_human: 'translated error message, readable for humans',
|
|
}
|
|
|
|
=end
|
|
|
|
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.send(
|
|
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, username incorrect!'),
|
|
'Lookup failed' => __('Authentication failed, username incorrect!'),
|
|
'Invalid credentials' => __('Authentication failed, invalid credentials!'),
|
|
'authentication not enabled' => __('Authentication not possible (not offered by the service)'),
|
|
'getaddrinfo: nodename nor servname provided, or not known' => __('Hostname not found!'),
|
|
'getaddrinfo: Name or service not known' => __('Hostname not found!'),
|
|
'No route to host' => __('No route to host!'),
|
|
'execution expired' => __('Host not reachable!'),
|
|
'Connection refused' => __('Connection refused!'),
|
|
}
|
|
end
|
|
|
|
end
|
|
|
|
end
|