# frozen_string_literal: true require "net/imap" require "net/smtp" require "net/pop" class EmailSettingsExceptionHandler EXPECTED_EXCEPTIONS = [ Net::POPAuthenticationError, Net::IMAP::NoResponseError, Net::IMAP::Error, Net::SMTPAuthenticationError, Net::SMTPServerBusy, Net::SMTPSyntaxError, Net::SMTPFatalError, Net::SMTPUnknownError, Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED, ] class GenericProvider def initialize(exception) @exception = exception end def message case @exception when Net::POPAuthenticationError net_pop_authentication_error when Net::IMAP::NoResponseError net_imap_no_response_error when Net::IMAP::Error net_imap_unhandled_error when Net::SMTPAuthenticationError net_smtp_authentication_error when Net::SMTPServerBusy net_smtp_server_busy when Net::SMTPSyntaxError, Net::SMTPFatalError, Net::SMTPUnknownError net_smtp_unhandled_error when SocketError, Errno::ECONNREFUSED socket_connection_error when Net::OpenTimeout, Net::ReadTimeout net_timeout_error else unhandled_error end end private def net_pop_authentication_error I18n.t("email_settings.pop3_authentication_error") end def net_imap_no_response_error # Most of IMAP's errors are lumped under the NoResponseError, including invalid # credentials errors, because it is raised when a "NO" response is # raised from the IMAP server https://datatracker.ietf.org/doc/html/rfc3501#section-7.1.2 # # Generally, it should be fairly safe to just return the error message as is. if @exception.message.match(/Invalid credentials/) I18n.t("email_settings.imap_authentication_error") else I18n.t( "email_settings.imap_no_response_error", message: @exception.message.gsub(" (Failure)", ""), ) end end def net_imap_unhandled_error I18n.t("email_settings.imap_unhandled_error", message: @exception.message) end def net_smtp_authentication_error I18n.t("email_settings.smtp_authentication_error") end def net_smtp_server_busy I18n.t("email_settings.smtp_server_busy_error") end def net_smtp_unhandled_error I18n.t("email_settings.smtp_unhandled_error", message: @exception.message) end def socket_connection_error I18n.t("email_settings.connection_error") end def net_timeout_error I18n.t("email_settings.timeout_error") end def unhandled_error I18n.t("email_settings.unhandled_error", message: @exception.message) end end class GmailProvider < GenericProvider def net_smtp_authentication_error # Gmail requires use of application-specific passwords when 2FA is enabled and return # a special error message calling this out. if @exception.message.match(/Application-specific password required/) I18n.t("email_settings.authentication_error_gmail_app_password") else super end end def net_imap_no_response_error # Gmail requires use of application-specific passwords when 2FA is enabled and return # a special error message calling this out. if @exception.message.match(/Application-specific password required/) I18n.t("email_settings.authentication_error_gmail_app_password") else super end end end def self.friendly_exception_message(exception, host) if host.include?("gmail.com") EmailSettingsExceptionHandler::GmailProvider.new(exception).message else EmailSettingsExceptionHandler::GenericProvider.new(exception).message end end end