discourse/app/services/email_settings_validator.rb
Michael Brown 2354eb3419 FIX: actually disable SMTP starttls if the user is attempting to disable it
starttls is implicitly enabled by default, we need to explicitly disable it if
the user wants to do so

Without doing so, the SMTP module would still attempt starttls if offered, but
potentially with the wrong context.

This has the potential side effect of leaving people with a non-working email
configuration if they previously attempted to disable starttls but actually
require it, so add a problem check to ensure that the admin is notified.
2025-11-04 23:42:14 -05:00

178 lines
5.9 KiB
Ruby

# frozen_string_literal: true
require "net/imap"
require "net/smtp"
require "net/pop"
# Usage:
#
# begin
# EmailSettingsValidator.validate_imap(host: "imap.test.com", port: 999, username: "test@test.com", password: "password")
#
# # or for specific host preset
# EmailSettingsValidator.validate_imap(**{ username: "test@gmail.com", password: "test" }.merge(Email.gmail_imap_settings))
#
# rescue *EmailSettingsExceptionHandler::EXPECTED_EXCEPTIONS => err
# EmailSettingsExceptionHandler.friendly_exception_message(err, host)
# end
class EmailSettingsValidator
def self.validate_as_user(user, protocol, **kwargs)
DistributedMutex.synchronize("validate_#{protocol}_#{user.id}", validity: 10) do
self.public_send("validate_#{protocol}", **kwargs)
end
end
##
# Attempts to authenticate and disconnect a POP3 session and if that raises
# an error then it is assumed the credentials or some other settings are wrong.
#
# @param debug [Boolean] - When set to true, any errors will be logged at a warning
# level before being re-raised.
def self.validate_pop3(
host:,
port:,
username:,
password:,
ssl: SiteSetting.pop3_polling_ssl,
openssl_verify: SiteSetting.pop3_polling_openssl_verify,
debug: Rails.env.development?
)
begin
pop3 = Net::POP3.new(host, port)
# Note that we do not allow which verification mode to be specified
# like we do for SMTP, we just pick TLS1_2 if the SSL and openSSL verify
# options have been enabled.
if ssl
if openssl_verify
pop3.enable_ssl(max_version: OpenSSL::SSL::TLS1_2_VERSION)
else
pop3.enable_ssl(OpenSSL::SSL::VERIFY_NONE)
end
end
# This disconnects itself, unlike SMTP and IMAP.
pop3.auth_only(username, password)
rescue => err
log_and_raise(err, debug)
end
end
##
# Attempts to start an SMTP session and if that raises an error then it is
# assumed the credentials or other settings are wrong.
#
# @param domain [String] - Used for HELO, should be the FQDN of the server sending the mail
# localhost can be used in development mode.
# See https://datatracker.ietf.org/doc/html/rfc788#section-4
# @param debug [Boolean] - When set to true, any errors will be logged at a warning
# level before being re-raised.
def self.validate_smtp(
host:,
port:,
username:,
password:,
domain: nil,
authentication: nil,
enable_starttls_auto: GlobalSetting.smtp_enable_start_tls,
enable_tls: GlobalSetting.smtp_force_tls,
openssl_verify_mode: GlobalSetting.smtp_openssl_verify_mode,
debug: Rails.env.development?
)
begin
if username || password
authentication = SmtpProviderOverrides.authentication_override(host) if authentication.nil?
authentication = authentication.to_sym
if !%i[plain login cram_md5].include?(authentication)
raise ArgumentError, "Invalid authentication method. Must be plain, login, or cram_md5."
end
else
authentication = nil
end
if domain.blank?
if Rails.env.development?
domain = "localhost"
else
# Because we are using the SMTP settings here to send emails,
# the domain should just be the TLD of the host.
domain = MiniSuffix.domain(host)
end
end
smtp = Net::SMTP.new(host, port)
# These SSL options are cribbed from the Mail gem, which is used internally
# by ActionMailer. Unfortunately the mail gem hides this setup in private
# methods, e.g. https://github.com/mikel/mail/blob/master/lib/mail/network/delivery_methods/smtp.rb#L112-L147
#
# Relying on the GlobalSetting options is a good idea here.
#
# For specific use cases, options should be passed in from higher up. For example
# Gmail needs either port 465 and tls enabled, or port 587 and starttls_auto.
if openssl_verify_mode.kind_of?(String)
openssl_verify_mode = OpenSSL::SSL.const_get("VERIFY_#{openssl_verify_mode.upcase}")
end
ssl_context = Net::SMTP.default_ssl_context
ssl_context.verify_mode = openssl_verify_mode if openssl_verify_mode
if enable_starttls_auto
# starttls is automatic, but we might need to change the context
smtp.enable_starttls_auto(ssl_context)
else
smtp.disable_starttls
end
smtp.enable_tls(ssl_context) if enable_tls
smtp.open_timeout = 5
# Some SMTP servers have a higher delay to respond with errors
# as a tarpit measure that slows down clients who are sending "bad" commands.
# 10s is the minimum, we might need to increase this in the future.
smtp.read_timeout = 10
smtp.start(domain, username, password, authentication)
smtp.finish
rescue => err
log_and_raise(err, debug)
end
end
##
# Attempts to login, logout, and disconnect an IMAP session and if that raises
# an error then it is assumed the credentials or some other settings are wrong.
#
# @param debug [Boolean] - When set to true, any errors will be logged at a warning
# level before being re-raised.
def self.validate_imap(
host:,
port:,
username:,
password:,
open_timeout: 5,
ssl: true,
debug: false
)
begin
imap = Net::IMAP.new(host, port: port, ssl: ssl, open_timeout: open_timeout)
imap.login(username, password)
begin
imap.logout
rescue StandardError
nil
end
imap.disconnect
rescue => err
log_and_raise(err, debug)
end
end
def self.log_and_raise(err, debug)
if debug
Rails.logger.warn(
"[EmailSettingsValidator] Error encountered when validating email settings: #{err.message} #{err.backtrace.join("\n")}",
)
end
raise err
end
end