Refactoring of email detection (added also tests now).

This commit is contained in:
Martin Edenhofer 2015-08-23 22:21:04 +02:00
parent 882d1cd5aa
commit 7070b1748c
8 changed files with 1611 additions and 709 deletions

View file

@ -193,415 +193,11 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password}
# check admin permissions
return if deny_if_not_role(Z_ROLENAME_ADMIN)
# validation
user = nil
domain = nil
if params[:email] =~ /^(.+?)@(.+?)$/
user = $1
domain = $2
end
if !user || !domain
render json: {
result: 'invalid',
messages: {
email: 'Invalid email.'
},
}
return
end
# check domain based attributes
provider_map = {
google: {
domain: 'gmail.com|googlemail.com|gmail.de',
inbound: {
adapter: 'imap',
options: {
host: 'imap.gmail.com',
port: '993',
ssl: true,
user: params[:email],
password: params[:password],
},
},
outbound: {
adapter: 'smtp',
options: {
host: 'smtp.gmail.com',
port: '25',
start_tls: true,
user: params[:email],
password: params[:password],
}
},
},
microsoft: {
domain: 'outlook.com|hotmail.com',
inbound: {
adapter: 'imap',
options: {
host: 'imap-mail.outlook.com',
port: '993',
ssl: true,
user: params[:email],
password: params[:password],
},
},
outbound: {
adapter: 'smtp',
options: {
host: 'smtp-mail.outlook.com',
port: 25,
start_tls: true,
user: params[:email],
password: params[:password],
}
},
},
}
# probe based on email domain and mx
domains = [domain]
mail_exchangers = mxers(domain)
if mail_exchangers && mail_exchangers[0]
logger.info "MX for #{domain}: #{mail_exchangers} - #{mail_exchangers[0][0]}"
end
if mail_exchangers && mail_exchangers[0] && mail_exchangers[0][0]
domains.push mail_exchangers[0][0]
end
provider_map.each {|_provider, settings|
domains.each {|domain_to_check|
next if domain_to_check !~ /#{settings[:domain]}/i
# probe inbound
result = email_probe_inbound( settings[:inbound] )
if result[:result] != 'ok'
render json: result
return # rubocop:disable Lint/NonLocalExitFromIterator
end
# probe outbound
result = email_probe_outbound( settings[:outbound], params[:email] )
if result[:result] != 'ok'
render json: result
return # rubocop:disable Lint/NonLocalExitFromIterator
end
render json: {
result: 'ok',
setting: settings,
}
return # rubocop:disable Lint/NonLocalExitFromIterator
}
}
# probe inbound
inbound_map = []
if mail_exchangers && mail_exchangers[0] && mail_exchangers[0][0]
inbound_mx = [
{
adapter: 'imap',
options: {
host: mail_exchangers[0][0],
port: 993,
ssl: true,
user: user,
password: params[:password],
},
},
{
adapter: 'imap',
options: {
host: mail_exchangers[0][0],
port: 993,
ssl: true,
user: params[:email],
password: params[:password],
},
},
]
inbound_map = inbound_map + inbound_mx
end
inbound_auto = [
{
adapter: 'imap',
options: {
host: "mail.#{domain}",
port: 993,
ssl: true,
user: user,
password: params[:password],
},
},
{
adapter: 'imap',
options: {
host: "mail.#{domain}",
port: 993,
ssl: true,
user: params[:email],
password: params[:password],
},
},
{
adapter: 'imap',
options: {
host: "imap.#{domain}",
port: 993,
ssl: true,
user: user,
password: params[:password],
},
},
{
adapter: 'imap',
options: {
host: "imap.#{domain}",
port: 993,
ssl: true,
user: params[:email],
password: params[:password],
},
},
{
adapter: 'pop3',
options: {
host: "mail.#{domain}",
port: 995,
ssl: true,
user: user,
password: params[:password],
},
},
{
adapter: 'pop3',
options: {
host: "mail.#{domain}",
port: 995,
ssl: true,
user: params[:email],
password: params[:password],
},
},
{
adapter: 'pop3',
options: {
host: "pop.#{domain}",
port: 995,
ssl: true,
user: user,
password: params[:password],
},
},
{
adapter: 'pop3',
options: {
host: "pop.#{domain}",
port: 995,
ssl: true,
user: params[:email],
password: params[:password],
},
},
{
adapter: 'pop3',
options: {
host: "pop3.#{domain}",
port: 995,
ssl: true,
user: user,
password: params[:password],
},
},
{
adapter: 'pop3',
options: {
host: "pop3.#{domain}",
port: 995,
ssl: true,
user: params[:email],
password: params[:password],
},
},
]
inbound_map = inbound_map + inbound_auto
settings = {}
success = false
inbound_map.each {|config|
logger.info "INBOUND PROBE: #{config.inspect}"
result = email_probe_inbound( config )
logger.info "INBOUND RESULT: #{result.inspect}"
next if result[:result] != 'ok'
success = true
settings[:inbound] = config
break
}
if !success
render json: {
result: 'failed',
}
return
end
# probe outbound
outbound_map = []
if mail_exchangers && mail_exchangers[0] && mail_exchangers[0][0]
outbound_mx = [
{
adapter: 'smtp',
options: {
host: mail_exchangers[0][0],
port: 25,
start_tls: true,
user: user,
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: mail_exchangers[0][0],
port: 25,
start_tls: true,
user: params[:email],
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: mail_exchangers[0][0],
port: 465,
start_tls: true,
user: user,
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: mail_exchangers[0][0],
port: 465,
start_tls: true,
user: params[:email],
password: params[:password],
},
},
]
outbound_map = outbound_map + outbound_mx
end
outbound_auto = [
{
adapter: 'smtp',
options: {
host: "mail.#{domain}",
port: 25,
start_tls: true,
user: user,
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: "mail.#{domain}",
port: 25,
start_tls: true,
user: params[:email],
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: "mail.#{domain}",
port: 465,
start_tls: true,
user: user,
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: "mail.#{domain}",
port: 465,
start_tls: true,
user: params[:email],
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: "smtp.#{domain}",
port: 25,
start_tls: true,
user: user,
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: "smtp.#{domain}",
port: 25,
start_tls: true,
user: params[:email],
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: "smtp.#{domain}",
port: 465,
start_tls: true,
user: user,
password: params[:password],
},
},
{
adapter: 'smtp',
options: {
host: "smtp.#{domain}",
port: 465,
start_tls: true,
user: params[:email],
password: params[:password],
},
},
]
success = false
outbound_map.each {|config|
logger.info "OUTBOUND PROBE: #{config.inspect}"
result = email_probe_outbound( config, params[:email] )
logger.info "OUTBOUND RESULT: #{result.inspect}"
next if result[:result] != 'ok'
success = true
settings[:outbound] = config
break
}
if !success
render json: {
result: 'failed',
}
return
end
render json: {
result: 'ok',
setting: settings,
}
# probe settings based on email and password
render json: EmailHelper::Probe.full(
email: params[:email],
password: params[:password],
)
end
def email_outbound
@ -609,18 +205,8 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password}
# check admin permissions
return if deny_if_not_role(Z_ROLENAME_ADMIN)
# validate params
if !params[:adapter]
render json: {
result: 'invalid',
}
return
end
# connection test
result = email_probe_outbound( params, params[:email] )
render json: result
render json: EmailHelper::Probe.outbound(params, params[:email])
end
def email_inbound
@ -628,18 +214,8 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password}
# check admin permissions
return if deny_if_not_role(Z_ROLENAME_ADMIN)
# validate params
if !params[:adapter]
render json: {
result: 'invalid',
}
return
end
# connection test
result = email_probe_inbound( params )
render json: result
render json: EmailHelper::Probe.inbound(params)
end
def email_verify
@ -653,295 +229,80 @@ curl http://localhost/api/v1/getting_started -v -u #{login}:#{password}
else
subject = params[:subject]
end
result = email_probe_outbound( params[:outbound], params[:meta][:email], subject )
(1..5).each {
sleep 10
result = EmailHelper::Verify.email(
outbound: params[:outbound],
inbound: params[:inbound],
sender: params[:meta][:email],
subject: subject,
)
# fetch mailbox
found = nil
# check delivery for 30 sek.
if result[:result] != 'ok'
render json: result
return
end
begin
if params[:inbound][:adapter] =~ /^imap$/i
found = Channel::IMAP.new.fetch( { options: params[:inbound][:options] }, 'verify', subject )
else
found = Channel::POP3.new.fetch( { options: params[:inbound][:options] }, 'verify', subject )
end
rescue => e
render json: {
result: 'invalid',
message: e.to_s,
subject: subject,
}
return
end
next if !found
next if found != 'verify ok'
# remember address
address = EmailAddress.where( email: params[:meta][:email] ).first
if !address
address = EmailAddress.first
end
if address
address.update_attributes(
realname: params[:meta][:realname],
email: params[:meta][:email],
active: 1,
updated_by_id: 1,
created_by_id: 1,
)
else
EmailAddress.create(
realname: params[:meta][:realname],
email: params[:meta][:email],
active: 1,
updated_by_id: 1,
created_by_id: 1,
)
end
# store mailbox
Channel.create(
area: 'Email::Inbound',
adapter: params[:inbound][:adapter],
options: params[:inbound][:options],
group_id: 1,
# remember address
address = EmailAddress.where( email: params[:meta][:email] ).first
if !address
address = EmailAddress.first
end
if address
address.update_attributes(
realname: params[:meta][:realname],
email: params[:meta][:email],
active: 1,
updated_by_id: 1,
created_by_id: 1,
)
else
EmailAddress.create(
realname: params[:meta][:realname],
email: params[:meta][:email],
active: 1,
updated_by_id: 1,
created_by_id: 1,
)
end
# save settings
if params[:outbound][:adapter] =~ /^smtp$/i
smtp = Channel.where( adapter: 'SMTP', area: 'Email::Outbound' ).first
smtp.options = params[:outbound][:options]
smtp.active = true
smtp.save!
sendmail = Channel.where( adapter: 'Sendmail' ).first
sendmail.active = false
sendmail.save!
else
sendmail = Channel.where( adapter: 'Sendmail', area: 'Email::Outbound' ).first
sendmail.options = {}
sendmail.active = true
sendmail.save!
smtp = Channel.where( adapter: 'SMTP' ).first
smtp.active = false
smtp.save
end
# store mailbox
Channel.create(
area: 'Email::Inbound',
adapter: params[:inbound][:adapter],
options: params[:inbound][:options],
group_id: 1,
active: 1,
updated_by_id: 1,
created_by_id: 1,
)
render json: {
result: 'ok',
}
return
}
# save settings
if params[:outbound][:adapter] =~ /^smtp$/i
smtp = Channel.where( adapter: 'SMTP', area: 'Email::Outbound' ).first
smtp.options = params[:outbound][:options]
smtp.active = true
smtp.save!
sendmail = Channel.where( adapter: 'Sendmail' ).first
sendmail.active = false
sendmail.save!
else
sendmail = Channel.where( adapter: 'Sendmail', area: 'Email::Outbound' ).first
sendmail.options = {}
sendmail.active = true
sendmail.save!
smtp = Channel.where( adapter: 'SMTP' ).first
smtp.active = false
smtp.save
end
# check delivery for 30 sek.
render json: {
result: 'invalid',
message: 'Verification Email not found in mailbox.',
subject: subject,
result: 'ok',
}
end
private
def email_probe_outbound(params, email, subject = nil)
# validate params
if !params[:adapter]
result = {
result: 'invalid',
message: 'Invalid, need adapter!',
}
return result
end
if subject
mail = {
:from => email,
:to => email,
:subject => "Zammad Getting started Test Email #{subject}",
:body => "This is a Test Email of Zammad to check if sending and receiving is working correctly.\n\nYou can ignore or delete this email.",
'x-zammad-ignore' => 'true',
}
else
mail = {
from: email,
to: 'emailtrytest@znuny.com',
subject: 'test',
body: 'test',
}
end
# test connection
translation_map = {
'authentication failed' => 'Authentication failed!',
'Incorrect username' => 'Authentication failed!',
'getaddrinfo: nodename nor servname provided, or not known' => 'Hostname not found!',
'No route to host' => 'No route to host!',
'Connection refused' => 'Connection refused!',
}
if params[:adapter] =~ /^smtp$/i
# in case, fill missing params
if !params[:options].key?(:port)
params[:options][:port] = 25
end
if !params[:options].key?(:ssl)
params[:options][:ssl] = true
end
begin
Channel::SMTP.new.send(
mail,
{
options: params[:options]
}
)
rescue => e
# check if sending email was ok, but mailserver rejected
if !subject
white_map = {
'Recipient address rejected' => true,
}
white_map.each {|key, _message|
next if e.message !~ /#{Regexp.escape(key)}/i
result = {
result: 'ok',
settings: params,
notice: e.message,
}
return result
}
end
message_human = ''
translation_map.each {|key, message|
if e.message =~ /#{Regexp.escape(key)}/i
message_human = message
end
}
result = {
result: 'invalid',
settings: params,
message: e.message,
message_human: message_human,
}
return result
end
result = {
result: 'ok',
}
return result
end
begin
Channel::Sendmail.new.send(
mail,
nil
)
rescue => e
message_human = ''
translation_map.each {|key, message|
if e.message =~ /#{Regexp.escape(key)}/i
message_human = message
end
}
result = {
result: 'invalid',
settings: params,
message: e.message,
message_human: message_human,
}
return result
end
result = {
result: 'ok',
}
result
end
def email_probe_inbound(params)
# validate params
if !params[:adapter]
fail 'need :adapter param'
end
# connection test
translation_map = {
'authentication failed' => 'Authentication failed!',
'Incorrect username' => 'Authentication failed!',
'getaddrinfo: nodename nor servname provided, or not known' => 'Hostname not found!',
'No route to host' => 'No route to host!',
'Connection refused' => 'Connection refused!',
}
if params[:adapter] =~ /^imap$/i
begin
Channel::IMAP.new.fetch( { options: params[:options] }, 'check' )
rescue => e
message_human = ''
translation_map.each {|key, message|
if e.message =~ /#{Regexp.escape(key)}/i
message_human = message
end
}
result = {
result: 'invalid',
settings: params,
message: e.message,
message_human: message_human,
}
return result
end
result = {
result: 'ok',
}
return result
end
begin
Channel::POP3.new.fetch( { options: params[:options] }, 'check' )
rescue => e
message_human = ''
translation_map.each {|key, message|
if e.message =~ /#{Regexp.escape(key)}/i
message_human = message
end
}
result = {
result: 'invalid',
settings: params,
message: e.message,
message_human: message_human,
}
return result
end
result = {
result: 'ok',
}
result
end
def mxers(domain)
begin
mxs = Resolv::DNS.open do |dns|
ress = dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
ress.map { |r| [r.exchange.to_s, IPSocket.getaddress(r.exchange.to_s), r.preference] }
end
rescue => e
logger.error e.message
logger.error e.backtrace.inspect
end
mxs
end
def auto_wizard_enabled_response
return false if !AutoWizard.enabled?

View file

@ -11,13 +11,13 @@ class Channel < ApplicationModel
# 'warning: toplevel constant Twitter referenced by Channel::Twitter' error e.g.
# so we have to convert the channel name to the filename via Rails String.underscore
# http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html
require "channel/#{channel[:adapter].underscore}"
require "channel/#{channel[:adapter].to_filename}"
channel_object = Object.const_get("Channel::#{channel[:adapter]}")
channel_object = Object.const_get("Channel::#{channel[:adapter].to_classname}")
channel_instance = channel_object.new
channel_instance.fetch(channel)
rescue => e
logger.error "Can't use Channel::#{channel[:adapter]}"
logger.error "Can't use Channel::#{channel[:adapter].to_classname}"
logger.error e.inspect
logger.error e.backtrace
end

View file

@ -2,7 +2,7 @@
require 'net/imap'
class Channel::IMAP < Channel::EmailParser
class Channel::Imap < Channel::EmailParser
def fetch (channel, check_type = '', verify_string = '')
ssl = true

View file

@ -2,7 +2,7 @@
require 'net/pop'
class Channel::POP3 < Channel::EmailParser
class Channel::Pop3 < Channel::EmailParser
def fetch (channel, check_type = '', verify_string = '')
ssl = true

614
lib/email_helper.rb Normal file
View file

@ -0,0 +1,614 @@
module EmailHelper
=begin
get mail parts
user, domain = EmailHelper.parse_email('somebody@example.com')
returns
[user, domain]
=end
def self.parse_email(email)
user = nil
domain = nil
if email =~ /^(.+?)@(.+?)$/
user = $1
domain = $2
end
[user, domain]
end
=begin
get list of providers with inbound and outbound settings
map = EmailHelper.provider(email, password)
returns
{
google: {
domain: 'gmail.com|googlemail.com|gmail.de',
inbound: {
adapter: 'imap',
options: {
host: 'imap.gmail.com',
port: 993,
ssl: true,
user: email,
password: password,
},
},
outbound: {
adapter: 'smtp',
options: {
host: 'smtp.gmail.com',
port: 25,
start_tls: true,
user: email,
password: password,
}
},
},
...
}
=end
def self.provider(email, password)
# check domain based attributes
provider_map = {
google: {
domain: 'gmail.com|googlemail.com|gmail.de',
inbound: {
adapter: 'imap',
options: {
host: 'imap.gmail.com',
port: 993,
ssl: true,
user: email,
password: password,
},
},
outbound: {
adapter: 'smtp',
options: {
host: 'smtp.gmail.com',
port: 25,
start_tls: true,
user: email,
password: password,
}
},
},
microsoft: {
domain: 'outlook.com|hotmail.com',
inbound: {
adapter: 'imap',
options: {
host: 'imap-mail.outlook.com',
port: 993,
ssl: true,
user: email,
password: password,
},
},
outbound: {
adapter: 'smtp',
options: {
host: 'smtp-mail.outlook.com',
port: 25,
start_tls: true,
user: email,
password: password,
}
},
},
}
provider_map
end
=begin
get possible inbound settings based on mx
map = EmailHelper.provider_inbound_mx(user, email, password, mx_domains)
returns
{
adapter: 'imap',
options: {
host: mx_domains[0],
port: 993,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'imap',
options: {
host: mx_domains[0],
port: 993,
ssl: true,
user: email,
password: password,
},
},
=end
def self.provider_inbound_mx(user, email, password, mx_domains)
inbound_mxs = []
mx_domains.each {|domain|
inbound_mx = [
{
adapter: 'imap',
options: {
host: domain,
port: 993,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'imap',
options: {
host: domain,
port: 993,
ssl: true,
user: email,
password: password,
},
},
]
puts "ll #{inbound_mx.inspect}"
inbound_mxs = inbound_mxs.concat(inbound_mx)
}
inbound_mxs
end
=begin
get possible inbound settings based on mx
map = EmailHelper.provider_inbound_mx(user, email, password, mx_domains)
returns
{
adapter: 'imap',
options: {
host: mx_domains[0],
port: 993,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'imap',
options: {
host: mx_domains[0],
port: 993,
ssl: true,
user: email,
password: password,
},
},
=end
def self.provider_inbound_mx(user, email, password, mx_domains)
inbounds = []
mx_domains.each {|domain|
inbound = [
{
adapter: 'imap',
options: {
host: domain,
port: 993,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'imap',
options: {
host: domain,
port: 993,
ssl: true,
user: email,
password: password,
},
},
]
inbounds = inbounds.concat(inbound)
}
inbounds
end
=begin
get possible inbound settings based on guess
map = EmailHelper.provider_inbound_guess(user, email, password, domain)
returns
{
adapter: 'imap',
options: {
host: "mail.#{domain}",
port: 993,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'imap',
options: {
host: "mail.#{domain}",
port: 993,
ssl: true,
user: email,
password: password,
},
},
...
=end
def self.provider_inbound_guess(user, email, password, domain)
inbound_mx = [
{
adapter: 'imap',
options: {
host: "mail.#{domain}",
port: 993,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'imap',
options: {
host: "mail.#{domain}",
port: 993,
ssl: true,
user: email,
password: password,
},
},
{
adapter: 'imap',
options: {
host: "imap.#{domain}",
port: 993,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'imap',
options: {
host: "imap.#{domain}",
port: 993,
ssl: true,
user: email,
password: password,
},
},
{
adapter: 'pop3',
options: {
host: "mail.#{domain}",
port: 995,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'pop3',
options: {
host: "mail.#{domain}",
port: 995,
ssl: true,
user: email,
password: password,
},
},
{
adapter: 'pop3',
options: {
host: "pop.#{domain}",
port: 995,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'pop3',
options: {
host: "pop.#{domain}",
port: 995,
ssl: true,
user: email,
password: password,
},
},
{
adapter: 'pop3',
options: {
host: "pop3.#{domain}",
port: 995,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'pop3',
options: {
host: "pop3.#{domain}",
port: 995,
ssl: true,
user: email,
password: password,
},
},
]
inbound_mx
end
=begin
get possible outbound settings based on mx
map = EmailHelper.provider_outbound_mx(user, email, password, mx_domains)
returns
{
adapter: 'smtp',
options: {
host: domain,
port: 25,
start_tls: true,
user: user,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: domain,
port: 25,
start_tls: true,
user: email,
password: password,
},
},
=end
def self.provider_outbound_mx(user, email, password, mx_domains)
outbound_mxs = []
mx_domains.each {|domain|
outbound_mx = [
{
adapter: 'smtp',
options: {
host: domain,
port: 25,
start_tls: true,
user: user,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: domain,
port: 25,
start_tls: true,
user: email,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: domain,
port: 465,
start_tls: true,
user: user,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: domain,
port: 465,
start_tls: true,
user: email,
password: password,
},
},
]
outbound_mxs = outbound_mxs.concat(outbound_mx)
}
outbound_mxs
end
=begin
get possible outbound settings based on guess
map = EmailHelper.provider_outbound_guess(user, email, password, domain)
returns
{
adapter: 'imap',
options: {
host: "mail.#{domain}",
port: 993,
ssl: true,
user: user,
password: password,
},
},
{
adapter: 'imap',
options: {
host: "mail.#{domain}",
port: 993,
ssl: true,
user: email,
password: password,
},
},
...
=end
def self.provider_outbound_guess(user, email, password, domain)
outbound_mx = [
{
adapter: 'smtp',
options: {
host: "mail.#{domain}",
port: 25,
start_tls: true,
user: user,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: "mail.#{domain}",
port: 25,
start_tls: true,
user: email,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: "mail.#{domain}",
port: 465,
start_tls: true,
user: user,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: "mail.#{domain}",
port: 465,
start_tls: true,
user: email,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: "smtp.#{domain}",
port: 25,
start_tls: true,
user: user,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: "smtp.#{domain}",
port: 25,
start_tls: true,
user: email,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: "smtp.#{domain}",
port: 465,
start_tls: true,
user: user,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: "smtp.#{domain}",
port: 465,
start_tls: true,
user: email,
password: password,
},
},
]
outbound_mx
end
=begin
get dns mx records of domain
mx_records = EmailHelper.mx_records('example.com')
returns
['mx1.example.com', 'mx2.example.com']
=end
def self.mx_records(domain)
mail_exchangers = mxers(domain)
if mail_exchangers && mail_exchangers[0]
Rails.logger.info "MX for #{domain}: #{mail_exchangers} - #{mail_exchangers[0][0]}"
end
mx_records = []
if mail_exchangers && mail_exchangers[0] && mail_exchangers[0][0]
mx_records.push mail_exchangers[0][0]
end
mx_records
end
def self.mxers(domain)
begin
mxs = Resolv::DNS.open do |dns|
ress = dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
ress.map { |r|
[r.exchange.to_s, IPSocket.getaddress(r.exchange.to_s), r.preference]
}
end
rescue => e
Rails.logger.error e.message
Rails.logger.error e.backtrace.inspect
end
mxs
end
end

376
lib/email_helper/probe.rb Normal file
View file

@ -0,0 +1,376 @@
module EmailHelper
class Probe
=begin
get result of probe
result = EmailHelper::Probe.full(
email: 'znuny@example.com',
password: 'somepassword',
)
returns on success
{
result: 'ok',
inbound: {
adapter: 'imap',
options: {
host: 'imap.gmail.com',
port: 993,
ssl: true,
user: 'some@example.com',
password: 'password',
},
},
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
result = {
result: 'invalid',
messages: {
email: 'Invalid email.'
},
}
return result
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 = domains.concat(mx_records)
provider_map.each {|_provider, settings|
domains.each {|domain_to_check|
next if domain_to_check !~ /#{settings[:domain]}/i
# probe inbound
result = EmailHelper::Probe.inbound(settings[:inbound])
next if result[:result] != 'ok'
# probe outbound
result = EmailHelper::Probe.outbound(settings[:outbound], params[:email])
next if result[:result] != 'ok'
result = {
result: 'ok',
setting: settings,
}
return result
}
}
# 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
settings = {}
success = false
inbound_map.each {|config|
Rails.logger.info "INBOUND PROBE: #{config.inspect}"
result = EmailHelper::Probe.inbound( config )
Rails.logger.info "INBOUND RESULT: #{result.inspect}"
next if result[:result] != 'ok'
success = true
settings[:inbound] = config
break
}
if !success
result = {
result: 'failed',
}
return result
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 {|config|
Rails.logger.info "OUTBOUND PROBE: #{config.inspect}"
result = EmailHelper::Probe.outbound( config, params[:email] )
Rails.logger.info "OUTBOUND RESULT: #{result.inspect}"
next if result[:result] != 'ok'
success = true
settings[:outbound] = config
break
}
if !success
result = {
result: 'failed',
}
return result
end
{
result: 'ok',
setting: settings,
}
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',
}
)
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',
},
message: 'error message from used lib',
message_human: 'translated error message, readable for humans',
}
=end
def self.inbound(params)
# validate params
if !params[:adapter]
result = {
result: 'invalid',
message: 'Invalid, need adapter!',
}
return result
end
# connection test
begin
if params[:adapter] =~ /^imap$/i
Channel::Imap.new.fetch( { options: params[:options] }, 'check' )
elsif params[:adapter] =~ /^pop3$/i
Channel::Pop3.new.fetch( { options: params[:options] }, 'check' )
else
fail "Invalid adapter '#{params[:adapter]}'"
end
rescue => e
message_human = ''
translations.each {|key, message|
if e.message =~ /#{Regexp.escape(key)}/i
message_human = message
end
}
result = {
result: 'invalid',
settings: params,
message: e.message,
message_human: message_human,
}
return result
end
result = {
result: 'ok',
}
result
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@example.com',
)
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)
# validate params
if !params[:adapter]
result = {
result: 'invalid',
message: 'Invalid, need adapter!',
}
return result
end
if subject
mail = {
:from => email,
:to => email,
:subject => "Zammad Getting started Test Email #{subject}",
:body => "This is a Test Email of Zammad to check if sending and receiving is working correctly.\n\nYou can ignore or delete this email.",
'x-zammad-ignore' => 'true',
}
else
mail = {
from: email,
to: 'emailtrytest@znuny.com',
subject: 'This is a Test Email',
body: "This is a Test Email of Zammad to verify if Zammad can send emails to an external address.\n\nIf you see this email, you can ignore and delete it.",
}
end
# test connection
begin
if params[:adapter] =~ /^smtp$/i
# in case, fill missing params
if !params[:options].key?(:port)
params[:options][:port] = 25
end
if !params[:options].key?(:ssl)
params[:options][:ssl] = true
end
Channel::SMTP.new.send(
mail,
{
options: params[:options]
}
)
elsif params[:adapter] =~ /^sendmail$/i
Channel::Sendmail.new.send(
mail,
nil
)
else
fail "Invalid adapter '#{params[:adapter]}'"
end
rescue => e
# check if sending email was ok, but mailserver rejected
if !subject
white_map = {
'Recipient address rejected' => true,
}
white_map.each {|key, _message|
next if e.message !~ /#{Regexp.escape(key)}/i
result = {
result: 'ok',
settings: params,
notice: e.message,
}
return result
}
end
message_human = ''
translations.each {|key, message|
if e.message =~ /#{Regexp.escape(key)}/i
message_human = message
end
}
result = {
result: 'invalid',
settings: params,
message: e.message,
message_human: message_human,
}
return result
end
result = {
result: 'ok',
}
result
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!',
'getaddrinfo: nodename nor servname provided, or 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

106
lib/email_helper/verify.rb Normal file
View file

@ -0,0 +1,106 @@
module EmailHelper
class Verify
=begin
get result of inbound probe
result = EmailHelper::Verify.email(
inbound: {
adapter: 'imap',
options: {
host: 'imap.gmail.com',
port: 993,
ssl: true,
user: 'some@example.com',
password: 'password',
},
},
outbound: {
adapter: 'smtp',
options: {
host: 'smtp.gmail.com',
port: 25,
ssl: true,
user: 'some@example.com',
password: 'password',
},
},
sender: 'sender@example.com',
)
returns on success
{
result: 'ok'
}
returns on fail
{
result: 'invalid',
message: 'Verification Email not found in mailbox.',
subject: subject,
}
or
{
result: 'invalid',
message: 'Authentication failed!.',
subject: subject,
}
=end
def self.email(params)
# send verify email
if !params[:subject]
subject = '#' + rand(99_999_999_999).to_s
else
subject = params[:subject]
end
result = EmailHelper::Probe.outbound( params[:outbound], params[:sender], subject )
# looking for verify email
(1..5).each {
sleep 10
# fetch mailbox
found = nil
begin
if params[:inbound][:adapter] =~ /^imap$/i
found = Channel::Imap.new.fetch( { options: params[:inbound][:options] }, 'verify', subject )
else
found = Channel::Pop3.new.fetch( { options: params[:inbound][:options] }, 'verify', subject )
end
rescue => e
render json: {
result: 'invalid',
message: e.to_s,
subject: subject,
}
return
end
next if !found
next if found != 'verify ok'
return {
result: 'ok',
}
}
{
result: 'invalid',
message: 'Verification Email not found in mailbox.',
subject: subject,
}
end
end
end

View file

@ -0,0 +1,445 @@
# encoding: utf-8
require 'test_helper'
class EmailHelperTest < ActiveSupport::TestCase
test 'a mx_records' do
domain = 'znuny.com'
mx_domains = EmailHelper.mx_records(domain)
assert_equal('arber.znuny.com', mx_domains[0])
end
test 'a email parser test' do
user, domain = EmailHelper.parse_email('somebody@example.com')
assert_equal('somebody', user)
assert_equal('example.com', domain)
user, domain = EmailHelper.parse_email('somebody+test@example.com')
assert_equal('somebody+test', user)
assert_equal('example.com', domain)
user, domain = EmailHelper.parse_email('somebody+testexample.com')
assert_not(user)
assert_not(domain)
end
test 'provider test' do
email = 'linus@kernel.org'
password = 'some_pw'
map = EmailHelper.provider(email, password)
assert_equal('imap', map[:google][:inbound][:adapter])
assert_equal('imap.gmail.com', map[:google][:inbound][:options][:host])
assert_equal(993, map[:google][:inbound][:options][:port])
assert_equal(email, map[:google][:inbound][:options][:user])
assert_equal(password, map[:google][:inbound][:options][:password])
assert_equal('smtp', map[:google][:outbound][:adapter])
assert_equal('smtp.gmail.com', map[:google][:outbound][:options][:host])
assert_equal(25, map[:google][:outbound][:options][:port])
assert_equal(true, map[:google][:outbound][:options][:start_tls])
assert_equal(email, map[:google][:outbound][:options][:user])
assert_equal(password, map[:google][:outbound][:options][:password])
end
test 'provider_inbound_mx' do
email = 'linus@znuny.com'
password = 'some_pw'
user, domain = EmailHelper.parse_email(email)
mx_domains = EmailHelper.mx_records(domain)
map = EmailHelper.provider_inbound_mx(user, email, password, mx_domains)
assert_equal('imap', map[0][:adapter])
assert_equal('arber.znuny.com', map[0][:options][:host])
assert_equal(993, map[0][:options][:port])
assert_equal(user, map[0][:options][:user])
assert_equal(password, map[0][:options][:password])
assert_equal('imap', map[1][:adapter])
assert_equal('arber.znuny.com', map[1][:options][:host])
assert_equal(993, map[1][:options][:port])
assert_equal(email, map[1][:options][:user])
assert_equal(password, map[1][:options][:password])
end
test 'provider_inbound_guess' do
email = 'linus@znuny.com'
password = 'some_pw'
user, domain = EmailHelper.parse_email(email)
map = EmailHelper.provider_inbound_guess(user, email, password, domain)
assert_equal('imap', map[0][:adapter])
assert_equal('mail.znuny.com', map[0][:options][:host])
assert_equal(993, map[0][:options][:port])
assert_equal(user, map[0][:options][:user])
assert_equal(password, map[0][:options][:password])
assert_equal('imap', map[1][:adapter])
assert_equal('mail.znuny.com', map[1][:options][:host])
assert_equal(993, map[1][:options][:port])
assert_equal(email, map[1][:options][:user])
assert_equal(password, map[1][:options][:password])
end
test 'provider_outbound_mx' do
email = 'linus@znuny.com'
password = 'some_pw'
user, domain = EmailHelper.parse_email(email)
mx_domains = EmailHelper.mx_records(domain)
map = EmailHelper.provider_outbound_mx(user, email, password, mx_domains)
assert_equal('smtp', map[0][:adapter])
assert_equal('arber.znuny.com', map[0][:options][:host])
assert_equal(25, map[0][:options][:port])
assert_equal(true, map[0][:options][:start_tls])
assert_equal(user, map[0][:options][:user])
assert_equal(password, map[0][:options][:password])
assert_equal('smtp', map[1][:adapter])
assert_equal('arber.znuny.com', map[1][:options][:host])
assert_equal(25, map[1][:options][:port])
assert_equal(true, map[1][:options][:start_tls])
assert_equal(email, map[1][:options][:user])
assert_equal(password, map[1][:options][:password])
end
test 'provider_outbound_guess' do
email = 'linus@znuny.com'
password = 'some_pw'
user, domain = EmailHelper.parse_email(email)
map = EmailHelper.provider_outbound_guess(user, email, password, domain)
assert_equal('smtp', map[0][:adapter])
assert_equal('mail.znuny.com', map[0][:options][:host])
assert_equal(25, map[0][:options][:port])
assert_equal(true, map[0][:options][:start_tls])
assert_equal(user, map[0][:options][:user])
assert_equal(password, map[0][:options][:password])
assert_equal('smtp', map[1][:adapter])
assert_equal('mail.znuny.com', map[1][:options][:host])
assert_equal(25, map[1][:options][:port])
assert_equal(true, map[1][:options][:start_tls])
assert_equal(email, map[1][:options][:user])
assert_equal(password, map[1][:options][:password])
end
test 'z probe_inbound' do
# network issues
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: 'not_existsing_host',
port: 993,
ssl: true,
user: 'some@example.com',
password: 'password',
}
)
assert_equal('invalid', result[:result])
assert_equal('Hostname not found!', result[:message_human])
assert_equal('not_existsing_host', result[:settings][:options][:host])
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: 'www.zammad.com',
port: 993,
ssl: true,
user: 'some@example.com',
password: 'password',
}
)
assert_equal('invalid', result[:result])
assert_equal('Connection refused!', result[:message_human])
assert_equal('www.zammad.com', result[:settings][:options][:host])
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: '172.42.42.42',
port: 993,
ssl: true,
user: 'some@example.com',
password: 'password',
}
)
assert_equal('invalid', result[:result])
assert_equal('Host not reachable!', result[:message_human])
assert_equal('172.42.42.42', result[:settings][:options][:host])
# gmail
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: 'imap.gmail.com',
port: 993,
ssl: true,
user: 'some@example.com',
password: 'password',
}
)
assert_equal('invalid', result[:result])
assert_equal('Authentication failed, username incorrect!', result[:message_human])
assert_equal('imap.gmail.com', result[:settings][:options][:host])
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: 'imap.gmail.com',
port: 993,
ssl: true,
user: 'frank.tailor05@googlemail.com',
password: 'password',
}
)
assert_equal('invalid', result[:result])
assert_equal('Authentication failed, invalid credentials!', result[:message_human])
assert_equal('imap.gmail.com', result[:settings][:options][:host])
# dovecot
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: 'arber.znuny.com',
port: 993,
ssl: true,
user: 'some@example.com',
password: 'password',
}
)
assert_equal('invalid', result[:result])
assert_equal('Authentication failed!', result[:message_human])
assert_equal('arber.znuny.com', result[:settings][:options][:host])
# realtest - test I
if !ENV['EMAILHELPER_MAILBOX_1']
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@znuny.com:somepass'"
return
end
mailbox_user = ENV['EMAILHELPER_MAILBOX_1'].split(':')[0]
mailbox_password = ENV['EMAILHELPER_MAILBOX_1'].split(':')[1]
user, domain = EmailHelper.parse_email(mailbox_user)
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: 'arber.znuny.com',
port: 993,
ssl: true,
user: user,
password: mailbox_password,
}
)
assert_equal('ok', result[:result])
end
test 'z probe_outbound' do
# network issues
result = EmailHelper::Probe.outbound(
{
adapter: 'smtp',
options: {
host: 'not_existsing_host',
port: 25,
start_tls: true,
user: 'some@example.com',
password: 'password',
}
},
'some@example.com',
)
assert_equal('invalid', result[:result])
assert_equal('Hostname not found!', result[:message_human])
assert_equal('not_existsing_host', result[:settings][:options][:host])
result = EmailHelper::Probe.outbound(
{
adapter: 'smtp',
options: {
host: 'www.zammad.com',
port: 25,
start_tls: true,
user: 'some@example.com',
password: 'password',
}
},
'some@example.com',
)
assert_equal('invalid', result[:result])
assert_equal('Connection refused!', result[:message_human])
assert_equal('www.zammad.com', result[:settings][:options][:host])
result = EmailHelper::Probe.outbound(
{
adapter: 'smtp',
options: {
host: '172.42.42.42',
port: 25,
start_tls: true,
user: 'some@example.com',
password: 'password',
}
},
'some@example.com',
)
assert_equal('invalid', result[:result])
assert_equal('Host not reachable!', result[:message_human])
assert_equal('172.42.42.42', result[:settings][:options][:host])
# gmail
result = EmailHelper::Probe.outbound(
{
adapter: 'smtp',
options: {
host: 'smtp.gmail.com',
port: 25,
start_tls: true,
user: 'some@example.com',
password: 'password',
}
},
'some@example.com',
)
assert_equal('invalid', result[:result])
assert_equal('Authentication failed!', result[:message_human])
assert_equal('smtp.gmail.com', result[:settings][:options][:host])
result = EmailHelper::Probe.outbound(
{
adapter: 'smtp',
options: {
host: 'smtp.gmail.com',
port: 25,
start_tls: true,
user: 'frank.tailor05@googlemail.com',
password: 'password',
}
},
'some@example.com',
)
assert_equal('invalid', result[:result])
assert_equal('Authentication failed!', result[:message_human])
assert_equal('smtp.gmail.com', result[:settings][:options][:host])
# dovecot
result = EmailHelper::Probe.outbound(
{
adapter: 'smtp',
options: {
host: 'arber.znuny.com',
port: 25,
start_tls: true,
user: 'some@example.com',
password: 'password',
}
},
'some@example.com',
)
assert_equal('invalid', result[:result])
assert_equal('Authentication failed!', result[:message_human])
assert_equal('arber.znuny.com', result[:settings][:options][:host])
# realtest - test I
if !ENV['EMAILHELPER_MAILBOX_1']
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@znuny.com:somepass'"
return
end
mailbox_user = ENV['EMAILHELPER_MAILBOX_1'].split(':')[0]
mailbox_password = ENV['EMAILHELPER_MAILBOX_1'].split(':')[1]
user, domain = EmailHelper.parse_email(mailbox_user)
result = EmailHelper::Probe.outbound(
{
adapter: 'smtp',
options: {
host: 'arber.znuny.com',
port: 25,
start_tls: true,
user: user,
password: mailbox_password,
}
},
mailbox_user,
)
assert_equal('ok', result[:result])
end
test 'zz probe' do
result = EmailHelper::Probe.full(
email: 'invalid_format',
password: 'somepass',
)
assert_equal('invalid', result[:result])
assert_not(result[:setting])
# realtest - test I
if !ENV['EMAILHELPER_MAILBOX_1']
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@znuny.com:somepass'"
end
mailbox_user = ENV['EMAILHELPER_MAILBOX_1'].split(':')[0]
mailbox_password = ENV['EMAILHELPER_MAILBOX_1'].split(':')[1]
result = EmailHelper::Probe.full(
email: mailbox_user,
password: mailbox_password,
)
assert_equal('ok', result[:result])
assert_equal('arber.znuny.com', result[:setting][:inbound][:options][:host])
assert_equal('arber.znuny.com', result[:setting][:outbound][:options][:host])
end
test 'zz verify' do
# realtest - test I
if !ENV['EMAILHELPER_MAILBOX_1']
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@znuny.com:somepass'"
end
mailbox_user = ENV['EMAILHELPER_MAILBOX_1'].split(':')[0]
mailbox_password = ENV['EMAILHELPER_MAILBOX_1'].split(':')[1]
user, domain = EmailHelper.parse_email(mailbox_user)
result = EmailHelper::Verify.email(
inbound: {
adapter: 'imap',
options: {
host: 'arber.znuny.com',
port: 993,
ssl: true,
user: user,
password: mailbox_password,
},
},
outbound: {
adapter: 'smtp',
options: {
host: 'arber.znuny.com',
port: 25,
start_tls: true,
user: user,
password: mailbox_password,
},
},
sender: mailbox_user,
)
assert_equal('ok', result[:result])
end
end