From 501cbf47d04c03cbcb81b569a9a558550dc06564 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Wed, 7 Aug 2024 12:14:14 +0300 Subject: [PATCH] updated greylist validator --- app/lib/greylist_checker.rb | 110 ++++++++++++++++++++++++++++++++++++ app/lib/smtp_validator.rb | 60 -------------------- 2 files changed, 110 insertions(+), 60 deletions(-) create mode 100644 app/lib/greylist_checker.rb delete mode 100644 app/lib/smtp_validator.rb diff --git a/app/lib/greylist_checker.rb b/app/lib/greylist_checker.rb new file mode 100644 index 0000000000..e5585cd6d1 --- /dev/null +++ b/app/lib/greylist_checker.rb @@ -0,0 +1,110 @@ +require 'net/smtp' +require 'resolv' + +class GreylistChecker + GREYLIST_CODES = ['450', '451', '452', '421', '471', '472'] + + DEFAULT_OPTIONS = { + max_attempts: 1, + retry_delay: 60, + sender_email: 'martin@internet.ee', + helo_domain: 'hole.ee', + debug: true + } + + def initialize(email, options = {}) + @email = email + @domain = email.split('@').last + @options = DEFAULT_OPTIONS.merge(options) + @debug = @options[:debug] + end + + def check + mx_servers = get_mx_servers + debug_print("MX servers for #{@domain}: #{mx_servers.join(', ')}") + return { status: :error, message: "Failed to find MX servers" } if mx_servers.empty? + + mx_servers.each do |mx_server| + result = check_server(mx_server) + return result unless result[:status] == :greylisted + end + + { status: :greylisted, message: "All attempts resulted in greylisting" } + end + + private + + def get_mx_servers + Resolv::DNS.open do |dns| + mx_records = dns.getresources(@domain, Resolv::DNS::Resource::IN::MX) + mx_records.sort_by(&:preference).map(&:exchange).map(&:to_s) + end + rescue => e + debug_print("Error getting MX servers: #{e.message}") + [] + end + + def check_server(mx_server) + attempts = 0 + while attempts < @options[:max_attempts] + result = smtp_check(mx_server) + debug_print("Attempt #{attempts + 1} result: #{result}") + + return result unless result[:status] == :greylisted + + attempts += 1 + if attempts < @options[:max_attempts] + debug_print("Waiting before next attempt: #{@options[:retry_delay]} seconds") + sleep @options[:retry_delay] + end + end + result + end + + def smtp_check(mx_server) + debug_print("Starting SMTP check for server: #{mx_server}") + + Net::SMTP.start(mx_server, 25) do |smtp| + smtp.debug_output = method(:debug_print) if @debug + + debug_print("ehlo #{@options[:helo_domain]}") + smtp.ehlo(@options[:helo_domain]) + + debug_print("mail from:<#{@options[:sender_email]}>") + smtp.mailfrom(@options[:sender_email]) + + debug_print("rcpt to:<#{@email}>") + begin + response = smtp.rcptto(@email) + debug_print(response.message) + if response.success? + return { status: :success, message: "Email accepted" } + else + code = response.status.to_s[0..2] + if GREYLIST_CODES.include?(code) + return { status: :greylisted, message: "Greylisting detected: #{response.message}" } + else + return { status: :invalid, message: "Address rejected: #{response.message}" } + end + end + rescue Net::SMTPFatalError => e + debug_print("Received SMTP fatal error: #{e.message}") + return { status: :invalid, message: "Address rejected: #{e.message}" } + rescue Net::SMTPServerBusy => e + debug_print("Received SMTP Server Busy error: #{e.message}") + if GREYLIST_CODES.any? { |code| e.message.start_with?(code) } + return { status: :greylisted, message: "Greylisting detected: #{e.message}" } + else + return { status: :error, message: "Temporary server error: #{e.message}" } + end + end + end + rescue => e + debug_print("Error during SMTP check: #{e.class} - #{e.message}") + { status: :error, message: "Error connecting to SMTP server: #{e.message}" } + end + + def debug_print(message) + puts message if @debug + end +end diff --git a/app/lib/smtp_validator.rb b/app/lib/smtp_validator.rb deleted file mode 100644 index 032cdb2393..0000000000 --- a/app/lib/smtp_validator.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'net/smtp' -require 'openssl' - -class SMTPValidator - def self.validate(email, options = {}) - domain = email.split('@').last - smtp_server = options[:smtp_server] || domain - smtp_port = options[:smtp_port] || 587 - helo_domain = options[:helo_domain] || 'localhost' - user_name = options[:user_name] - password = options[:password] - from_address = options[:from_address] || user_name - auth_methods = options[:auth_methods] || [:plain, :login, :cram_md5] - - result = { valid: false, code: nil, message: nil, auth_method: nil } - - begin - smtp = Net::SMTP.new(smtp_server, smtp_port) - smtp.enable_starttls_auto - smtp.open_timeout = 5 - smtp.read_timeout = 5 - - auth_methods.each do |method| - begin - smtp.start(helo_domain, user_name, password, method) do |smtp| - from_response = smtp.mailfrom(from_address) - result[:code], result[:message] = from_response.string.split(' ', 2) - - to_response = smtp.rcptto(email) - result[:code], result[:message] = to_response.string.split(' ', 2) - result[:valid] = result[:code].to_i == 250 - end - result[:auth_method] = method - break - rescue Net::SMTPAuthenticationError => e - puts "Authentication failed with method #{method}: #{e.message}" - result[:code], result[:message] = e.message.split(' ', 2) - next - rescue Net::SMTPFatalError => e - result[:code], result[:message] = e.message.split(' ', 2) - break - end - end - rescue => e - puts "Connection Error: #{e.message}" - result[:message] = e.message - ensure - smtp.finish if smtp && smtp.started? - end - - if result[:auth_method] - puts "Email #{email} validation completed (authenticated with #{result[:auth_method]})" - else - puts "Failed to authenticate with any method" - end - - puts "Valid: #{result[:valid]}, Code: #{result[:code]}, Message: #{result[:message]}" - result - end -end