/api/email endpoints | (ns metabase.api.email (:require [clojure.data :as data] [clojure.set :as set] [clojure.string :as str] [compojure.core :refer [DELETE POST PUT]] [metabase.api.common :as api] [metabase.api.common.validation :as validation] [metabase.email :as email] [metabase.models.setting :as setting] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log])) |
(set! *warn-on-reflection* true) | |
(def ^:private mb-to-smtp-settings {:email-smtp-host :host :email-smtp-username :user :email-smtp-password :pass :email-smtp-port :port :email-smtp-security :security :email-from-name :sender-name :email-from-address :sender :email-reply-to :reply-to}) | |
Convert raw error message responses from our email functions into our normal api error response structure. | (defn- humanize-error-messages [{::email/keys [error]}] (when error (let [conn-error {:errors {:email-smtp-host "Wrong host or port" :email-smtp-port "Wrong host or port"}} creds-error {:errors {:email-smtp-username "Wrong username or password" :email-smtp-password "Wrong username or password"}} exceptions (u/full-exception-chain error) message (str/join ": " (map ex-message exceptions)) match-error (fn match-error [regex-or-exception-class [message exceptions]] (cond (instance? java.util.regex.Pattern regex-or-exception-class) (re-find regex-or-exception-class message) (class? regex-or-exception-class) (some (partial instance? regex-or-exception-class) exceptions)))] (log/warn "Problem connecting to mail server:" message) (condp match-error [message exceptions] ;; bad host = "Unknown SMTP host: foobar" #"^Unknown SMTP host:.*$" conn-error ;; host seems valid, but host/port failed connection = "Could not connect to SMTP host: localhost, port: 123" #".*Could(?: not)|(?:n't) connect to (?:SMTP )?host.*" conn-error ;; seen this show up on mandrill #"^Invalid Addresses$" creds-error ;; seen this show up on mandrill using TLS with bad credentials #"^failed to connect, no password specified\?$" creds-error ;; madrill authentication failure #"^435 4.7.8 Error: authentication failed:.*$" creds-error javax.mail.AuthenticationFailedException creds-error ;; everything else :( {:message (str "Sorry, something went wrong. Please try again. Error: " message)})))) |
Formats warnings when security settings are autocorrected. | (defn- humanize-email-corrections [corrections] (into {} (for [[k v] corrections] [k (tru "{0} was autocorrected to {1}" (name (mb-to-smtp-settings k)) (u/upper-case-en v))]))) |
Returns a map of setting names (keywords) and env var values. If an env var is not set, the setting is not included in the result. | (defn- env-var-values-by-email-setting [] (into {} (for [setting-name (keys mb-to-smtp-settings) :let [value (setting/env-var-value setting-name)] :when (some? value)] [setting-name value]))) |
/ | (api/defendpoint PUT "Update multiple email Settings. You must be a superuser or have `setting` permission to do this." [:as {settings :body}] {settings :map} (validation/check-has-application-permission :setting) (let [;; the frontend has access to an obfuscated version of the password. Watch for whether it sent us a new password or ;; the obfuscated version obfuscated? (and (:email-smtp-password settings) (email/email-smtp-password) (= (:email-smtp-password settings) (setting/obfuscate-value (email/email-smtp-password)))) ;; override `nil` values in the request with environment variables for testing the SMTP connection env-var-settings (env-var-values-by-email-setting) settings (merge settings env-var-settings) settings (-> (cond-> settings obfuscated? (assoc :email-smtp-password (email/email-smtp-password))) (select-keys (keys mb-to-smtp-settings)) (set/rename-keys mb-to-smtp-settings)) settings (cond-> settings (string? (:port settings)) (update :port #(Long/parseLong ^String %)) (string? (:security settings)) (update :security keyword)) response (email/test-smtp-connection settings)] (if-not (::email/error response) ;; test was good, save our settings (let [[_ corrections] (data/diff settings response) new-settings (set/rename-keys response (set/map-invert mb-to-smtp-settings))] ;; don't update settings if they are set by environment variables (setting/set-many! (apply dissoc new-settings (keys env-var-settings))) (cond-> (assoc new-settings :with-corrections (-> corrections (set/rename-keys (set/map-invert mb-to-smtp-settings)) humanize-email-corrections)) obfuscated? (update :email-smtp-password setting/obfuscate-value))) ;; test failed, return response message {:status 400 :body (humanize-error-messages response)}))) |
/ | (api/defendpoint DELETE "Clear all email related settings. You must be a superuser or have `setting` permission to do this." [] (validation/check-has-application-permission :setting) (setting/set-many! (zipmap (keys mb-to-smtp-settings) (repeat nil))) api/generic-204-no-content) |
/test | (api/defendpoint POST "Send a test email using the SMTP Settings. You must be a superuser or have `setting` permission to do this. Returns `{:ok true}` if we were able to send the message successfully, otherwise a standard 400 error response." [] (validation/check-has-application-permission :setting) (when-not (and (email/email-smtp-port) (email/email-smtp-host)) {:status 400 :body "Wrong host or port"}) (let [response (email/send-message-or-throw! {:subject "Metabase Test Email" :recipients [(:email @api/*current-user*)] :message-type :text :message "Your Metabase emails are working — hooray!"})] (if-not (::email/error response) {:ok true} {:status 400 :body (humanize-error-messages response)}))) |
(api/define-routes) | |