mirror of
https://github.com/discourse/discourse.git
synced 2024-12-05 08:40:29 +08:00
964da21817
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory. ### UI Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP. You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator. If there is an issue with the configuration or credentials a meaningful error message should be shown. IMAP settings must also be validated when IMAP is enabled, before saving. When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved): ### Database & Backend This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group. For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous. There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
187 lines
6.2 KiB
Ruby
187 lines
6.2 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.
|
|
#
|
|
# For Gmail, the port should be 587, enable_starttls_auto should be true,
|
|
# and enable_tls should be false.
|
|
#
|
|
# @param domain [String] - Used for HELO, will be the email sender's domain, so often
|
|
# will just be the host e.g. the domain for test@gmail.com is gmail.com.
|
|
# 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: GlobalSetting.smtp_authentication,
|
|
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
|
|
port, enable_tls, enable_starttls_auto = provider_specific_ssl_overrides(
|
|
host, port, enable_tls, enable_starttls_auto
|
|
)
|
|
|
|
if enable_tls && enable_starttls_auto
|
|
raise ArgumentError, "TLS and STARTTLS are mutually exclusive"
|
|
end
|
|
|
|
if ![:plain, :login, :cram_md5].include?(authentication.to_sym)
|
|
raise ArgumentError, "Invalid authentication method. Must be plain, login, or cram_md5."
|
|
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
|
|
|
|
smtp.enable_starttls_auto(ssl_context) if enable_starttls_auto
|
|
smtp.enable_tls(ssl_context) if enable_tls
|
|
|
|
smtp.open_timeout = 5
|
|
smtp.read_timeout = 5
|
|
|
|
smtp.start(domain, username, password, authentication.to_sym)
|
|
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)
|
|
imap.logout rescue nil
|
|
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
|
|
|
|
def self.provider_specific_ssl_overrides(host, port, enable_tls, enable_starttls_auto)
|
|
# Gmail acts weirdly if you do not use the correct combinations of
|
|
# TLS settings based on the port, we clean these up here for the user.
|
|
if host == "smtp.gmail.com"
|
|
if port.to_i == 587
|
|
enable_starttls_auto = true
|
|
enable_tls = false
|
|
elsif port.to_i == 465
|
|
enable_starttls_auto = false
|
|
enable_tls = true
|
|
end
|
|
end
|
|
|
|
[port, enable_tls, enable_starttls_auto]
|
|
end
|
|
end
|