diff --git a/index.html b/index.html index af37e2d..e0ba574 100644 --- a/index.html +++ b/index.html @@ -13842,11 +13842,14 @@

Important Libraries

Returns `{:ok true}` if we were able to send the message successfully, otherwise a standard 400 error response." [] (validation/check-has-application-permission :setting) - (let [response (email/send-message! - :subject "Metabase Test Email" + (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!")] + :message "Your Metabase emails are working — hooray!"})] (if-not (::email/error response) {:ok true} {:status 400 @@ -38857,6 +38860,7 @@
`:abstract?` (default = false)
[metabase.util.log :as log] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] + [metabase.util.retry :as retry] [postal.core :as postal] [postal.support :refer [make-props]]) (:import @@ -38959,34 +38963,39 @@
`:abstract?` (default = false)
(if (= message-type :attachments) (and (sequential? message) (every? map? message)) (string? message)))]])

Send an email to one or more recipients. Upon success, this returns the message that was just sent. This function -does not catch and swallow thrown exceptions, it will bubble up.

-
(mu/defn send-message-or-throw!
+does not catch and swallow thrown exceptions, it will bubble up. Should prefer to use [[send-email-retrying!]] unless
+the caller has its own retry logic.

+
(defn send-message-or-throw!
   {:style/indent 0}
-  [{:keys [subject recipients message-type message] :as email} :- EmailMessage]
+  [{:keys [subject recipients message-type message] :as email}]
   (try
-   (when-not (email-smtp-host)
-     (throw (ex-info (tru "SMTP host is not set.") {:cause :smtp-host-not-set})))
-   ;; Now send the email
-   (let [to-type (if (:bcc? email) :bcc :to)]
-     (send-email! (smtp-settings)
-                  (merge
-                   {:from    (if-let [from-name (email-from-name)]
-                               (str from-name " <" (email-from-address) ">")
-                               (email-from-address))
-                    to-type  recipients
-                    :subject subject
-                    :body    (case message-type
-                               :attachments message
-                               :text        message
-                               :html        [{:type    "text/html; charset=utf-8"
-                                              :content message}])}
-                   (when-let [reply-to (email-reply-to)]
-                     {:reply-to reply-to}))))
-   (catch Throwable e
-     (prometheus/inc :metabase-email/message-errors)
-     (throw e))
-   (finally
-    (prometheus/inc :metabase-email/messages))))

Schema for the response returned by various functions in [[metabase.email]]. Response will be a map with the key + (when-not (email-smtp-host) + (throw (ex-info (tru "SMTP host is not set.") {:cause :smtp-host-not-set}))) + ;; Now send the email + (let [to-type (if (:bcc? email) :bcc :to)] + (send-email! (smtp-settings) + (merge + {:from (if-let [from-name (email-from-name)] + (str from-name " <" (email-from-address) ">") + (email-from-address)) + to-type recipients + :subject subject + :body (case message-type + :attachments message + :text message + :html [{:type "text/html; charset=utf-8" + :content message}])} + (when-let [reply-to (email-reply-to)] + {:reply-to reply-to})))) + (catch Throwable e + (prometheus/inc :metabase-email/message-errors) + (when (not= :smtp-host-not-set (:cause (ex-data e))) + (throw e))) + (finally + (prometheus/inc :metabase-email/messages))))

Like [[send-message-or-throw!]] but retries sending on errors according to the retry settings.

+
(mu/defn send-email-retrying!
+  [email :- EmailMessage]
+  ((retry/decorate send-message-or-throw!) email))

Schema for the response returned by various functions in [[metabase.email]]. Response will be a map with the key :metabase.email/error, which will either be nil (indicating no error) or an instance of [[java.lang.Throwable]] with the error.

(def ^:private SMTPStatus
@@ -38995,17 +39004,17 @@ 
`:abstract?` (default = false)
either :text or :html or :attachments.

(email/send-message! - :subject "[Metabase] Password Reset Request" - :recipients ["cam@metabase.com"] - :message-type :text - :message "How are you today?")

+ {:subject "[Metabase] Password Reset Request" + :recipients ["cam@metabase.com"] + :message-type :text + :message "How are you today?")}

Upon success, this returns the :message that was just sent. (TODO -- confirm this.) This function will catch and log any exception, returning a [[SMTPStatus]].

(defn send-message!
   [& {:as msg-args}]
   (try
-    (send-message-or-throw! msg-args)
+    (send-email-retrying! msg-args)
     (catch Throwable e
       (log/warn e (trs "Failed to send email"))
       {::error e})))
@@ -39188,10 +39197,10 @@
`:abstract?` (default = false)
:logoHeader true :sentFromSetup sent-from-setup?}))] (email/send-message! - :subject (str (trs "You''re invited to join {0}''s {1}" company (app-name-trs))) - :recipients [(:email invited)] - :message-type :html - :message message-body)))

Return a sequence of email addresses for all Admin users.

+ {:subject (str (trs "You''re invited to join {0}''s {1}" company (app-name-trs))) + :recipients [(:email invited)] + :message-type :html + :message message-body})))

Return a sequence of email addresses for all Admin users.

The first recipient will be the site admin (or oldest admin if unset), which is the address that should be used in mailto links (e.g., for the new user to email with any questions).

@@ -39205,20 +39214,20 @@
`:abstract?` (default = false)
{:pre [(map? new-user)]} (let [recipients (all-admin-recipients)] (email/send-message! - :subject (str (if google-auth? - (trs "{0} created a {1} account" (:common_name new-user) (app-name-trs)) - (trs "{0} accepted their {1} invite" (:common_name new-user) (app-name-trs)))) - :recipients recipients - :message-type :html - :message (stencil/render-file "metabase/email/user_joined_notification" - (merge (common-context) - {:logoHeader true - :joinedUserName (or (:first_name new-user) (:email new-user)) - :joinedViaSSO google-auth? - :joinedUserEmail (:email new-user) - :joinedDate (t/format "EEEE, MMMM d" (t/zoned-date-time)) ; e.g. "Wednesday, July 13". TODO - is this what we want? - :adminEmail (first recipients) - :joinedUserEditUrl (str (public-settings/site-url) "/admin/people")})))))

Format and send an email informing the user how to reset their password.

+ {:subject (str (if google-auth? + (trs "{0} created a {1} account" (:common_name new-user) (app-name-trs)) + (trs "{0} accepted their {1} invite" (:common_name new-user) (app-name-trs)))) + :recipients recipients + :message-type :html + :message (stencil/render-file "metabase/email/user_joined_notification" + (merge (common-context) + {:logoHeader true + :joinedUserName (or (:first_name new-user) (:email new-user)) + :joinedViaSSO google-auth? + :joinedUserEmail (:email new-user) + :joinedDate (t/format "EEEE, MMMM d" (t/zoned-date-time)) ; e.g. "Wednesday, July 13". TODO - is this what we want? + :adminEmail (first recipients) + :joinedUserEditUrl (str (public-settings/site-url) "/admin/people")}))})))

Format and send an email informing the user how to reset their password.

(defn send-password-reset-email!
   [email sso-source password-reset-url is-active?]
   {:pre [(u/email? email)
@@ -39236,10 +39245,10 @@ 
`:abstract?` (default = false)
:adminEmail (public-settings/admin-email) :adminEmailSet (boolean (public-settings/admin-email))}))] (email/send-message! - :subject (trs "[{0}] Password Reset Request" (app-name-trs)) - :recipients [email] - :message-type :html - :message message-body)))

Format and send an email informing the user that this is the first time we've seen a login from this device. Expects + {:subject (trs "[{0}] Password Reset Request" (app-name-trs)) + :recipients [email] + :message-type :html + :message message-body})))

Format and send an email informing the user that this is the first time we've seen a login from this device. Expects login history information as returned by metabase.models.login-history/human-friendly-infos.

(mu/defn send-login-from-new-device-email!
   [{user-id :user_id, :keys [timestamp], :as login-history} :- [:map [:user_id pos-int?]]]
@@ -39256,10 +39265,10 @@ 
`:abstract?` (default = false)
message-body (stencil/render-file "metabase/email/login_from_new_device" context)] (email/send-message! - :subject (trs "We''ve Noticed a New {0} Login, {1}" (app-name-trs) (:first-name user-info)) - :recipients [(:email user-info)] - :message-type :html - :message message-body)))

Find emails for users that have an interest in monitoring the database. + {:subject (trs "We''ve Noticed a New {0} Login, {1}" (app-name-trs) (:first-name user-info)) + :recipients [(:email user-info)] + :message-type :html + :message message-body})))

Find emails for users that have an interest in monitoring the database. If oss that means admin users. If ee that also means users with monitoring and details permissions.

(defn- admin-or-ee-monitoring-details-emails
@@ -39320,10 +39329,10 @@ 
`:abstract?` (default = false)
(merge (common-context) context))] (when (seq emails) (email/send-message! - :subject (trs "[{0}] Model cache refresh failed for {1}" (app-name-trs) (:name database)) - :recipients (vec emails) - :message-type :html - :message message-body))))

Format and send an email to the system admin following up on the installation.

+ {:subject (trs "[{0}] Model cache refresh failed for {1}" (app-name-trs) (:name database)) + :recipients (vec emails) + :message-type :html + :message message-body}))))

Format and send an email to the system admin following up on the installation.

(defn send-follow-up-email!
   [email]
   {:pre [(u/email? email)]}
@@ -39613,7 +39622,7 @@ 
`:abstract?` (default = false)
[user subject template-path template-context] (future (try - (email/send-message-or-throw! + (email/send-email-retrying! {:recipients [(:email user)] :message-type :html :subject subject @@ -75479,7 +75488,6 @@
`:fallback`
(:require [clojure.string :as str] [metabase.api.common :as api] - [metabase.config :as config] [metabase.email :as email] [metabase.email.messages :as messages] [metabase.events :as events] @@ -75490,7 +75498,6 @@
`:fallback`
[metabase.models.interface :as mi] [metabase.models.pulse :as pulse :refer [Pulse]] [metabase.models.serialization :as serdes] - [metabase.models.setting :refer [defsetting]] [metabase.public-settings :as public-settings] [metabase.pulse.markdown :as markdown] [metabase.pulse.parameters :as pulse-params] @@ -75502,7 +75509,7 @@
`:fallback`
[metabase.server.middleware.session :as mw.session] [metabase.shared.parameters.parameters :as shared.params] [metabase.util :as u] - [metabase.util.i18n :refer [deferred-tru trs tru]] + [metabase.util.i18n :refer [trs tru]] [metabase.util.log :as log] [metabase.util.malli :as mu] [metabase.util.retry :as retry] @@ -75943,72 +75950,14 @@
`:fallback`
(defmethod send-notification! :email
   [emails]
   (doseq [{:keys [subject recipients message-type message]} emails]
-    (try
-      (email/send-message-or-throw! {:subject      subject
-                                     :recipients   recipients
-                                     :message-type message-type
-                                     :message      message
-                                     :bcc?         (email/bcc-enabled?)})
-      (catch ExceptionInfo e
-        (when (not= :smtp-host-not-set (:cause (ex-data e)))
-          (throw e))))))
-
(declare ^:private reconfigure-retrying)
-
(defsetting notification-retry-max-attempts
-  (deferred-tru "The maximum number of attempts for delivering a single notification.")
-  :type :integer
-  :default 7
-  :on-change reconfigure-retrying)
-
(defsetting notification-retry-initial-interval
-  (deferred-tru "The initial retry delay in milliseconds when delivering notifications.")
-  :type :integer
-  :default 500
-  :on-change reconfigure-retrying)
-
(defsetting notification-retry-multiplier
-  (deferred-tru "The delay multiplier between attempts to deliver a single notification.")
-  :type :double
-  :default 2.0
-  :on-change reconfigure-retrying)
-
(defsetting notification-retry-randomization-factor
-  (deferred-tru "The randomization factor of the retry delay when delivering notifications.")
-  :type :double
-  :default 0.1
-  :on-change reconfigure-retrying)
-
(defsetting notification-retry-max-interval-millis
-  (deferred-tru "The maximum delay between attempts to deliver a single notification.")
-  :type :integer
-  :default 30000
-  :on-change reconfigure-retrying)
-
(defn- retry-configuration []
-  (cond-> {:max-attempts (notification-retry-max-attempts)
-           :initial-interval-millis (notification-retry-initial-interval)
-           :multiplier (notification-retry-multiplier)
-           :randomization-factor (notification-retry-randomization-factor)
-           :max-interval-millis (notification-retry-max-interval-millis)}
-    (or config/is-dev? config/is-test?) (assoc :max-attempts 1)))

Returns a notification sender wrapping [[send-notifications!]] retrying -according to retry-configuration.

-
(defn- make-retry-state
-  []
-  (let [retry (retry/random-exponential-backoff-retry "send-notification-retry"
-                                                      (retry-configuration))]
-    {:retry retry
-     :sender (retry/decorate send-notification! retry)}))

Stores the current retry state. Updated whenever the notification -retry settings change. -It starts with value nil but is set whenever the settings change or when -the first call with retry is made. (See #22790 for more details.)

-
(defonce
-  ^{:private true
-    :doc }
-  retry-state
-  (atom nil))
-
(defn- reconfigure-retrying [_old-value _new-value]
-  (log/info (trs "Reconfiguring notification sender"))
-  (reset! retry-state (make-retry-state)))

Like [[send-notification!]] but retries sending on errors according -to the retry settings.

+ (email/send-message-or-throw! {:subject subject + :recipients recipients + :message-type message-type + :message message + :bcc? (email/bcc-enabled?)})))

Like [[send-notification!]] but retries sending on errors according to the retry settings.

(defn- send-notification-retrying!
   [& args]
-  (when-not @retry-state
-    (compare-and-set! retry-state nil (make-retry-state)))
-  (apply (:sender @retry-state) args))
+ (apply (retry/decorate send-notification!) args))
(defn- send-notifications! [notifications]
   (doseq [notification notifications]
     ;; do a try-catch around each notification so if one fails, we'll still send the other ones for example, an Alert
@@ -96936,7 +96885,7 @@ 

Quartz JavaDoc

(let [legacy-pulse (->> (t2/select :model/Pulse :dashboard_id nil :alert_condition nil :archived false) (map #(assoc % :url (urls/legacy-pulse-url (:id %)))))] (doseq [admin (t2/select :model/User :is_superuser true)] - (email/send-message-or-throw! + (email/send-email-retrying! {:recipients [(:email admin)] :message-type :html :subject "[Metabase] Removal of legacy pulses in upcoming Metabase release" @@ -103359,11 +103308,39 @@

Quartz JavaDoc

([x] (re-pattern (rx* x))) ([x & more] (rx (into [:and x] more))))))
 

metabase.util.retry

toc

Support for in-memory, thread-blocking retrying.

(ns metabase.util.retry
+  (:require [metabase.models.setting :refer [defsetting]]
+            [metabase.util.i18n :refer [deferred-tru]])
   (:import
    (io.github.resilience4j.core IntervalFunction)
    (io.github.resilience4j.retry Retry RetryConfig)
    (java.util.function Predicate)))
(set! *warn-on-reflection* true)
+
(defsetting retry-max-attempts
+  (deferred-tru "The maximum number of attempts for an event.")
+  :type :integer
+  :default 7)
+
(defsetting retry-initial-interval
+  (deferred-tru "The initial retry delay in milliseconds.")
+  :type :integer
+  :default 500)
+
(defsetting retry-multiplier
+  (deferred-tru "The delay multiplier between attempts.")
+  :type :double
+  :default 2.0)
+
(defsetting retry-randomization-factor
+  (deferred-tru "The randomization factor of the retry delay.")
+  :type :double
+  :default 0.1)
+
(defsetting retry-max-interval-millis
+  (deferred-tru "The maximum delay between attempts.")
+  :type :integer
+  :default 30000)
+
(defn- retry-configuration []
+  {:max-attempts (retry-max-attempts)
+   :initial-interval-millis (retry-initial-interval)
+   :multiplier (retry-multiplier)
+   :randomization-factor (retry-randomization-factor)
+   :max-interval-millis (retry-max-interval-millis)})
(defn- make-predicate [f]
   (reify Predicate (test [_ x] (f x))))

Returns a randomized exponential backoff retry named retry-name configured according the options in the second parameter.

@@ -103372,12 +103349,7 @@

Quartz JavaDoc

{:keys [^long max-attempts ^long initial-interval-millis ^double multiplier ^double randomization-factor ^long max-interval-millis - retry-on-result-pred retry-on-exception-pred] - :or {max-attempts 3 - initial-interval-millis 500 - multiplier 1.5 - randomization-factor 0.5 - max-interval-millis Long/MAX_VALUE}}] + retry-on-result-pred retry-on-exception-pred]}] (let [interval-fn (IntervalFunction/ofExponentialRandomBackoff initial-interval-millis multiplier randomization-factor max-interval-millis) @@ -103391,12 +103363,15 @@

Quartz JavaDoc

(.retryOnException (make-predicate retry-on-exception-pred)))] (Retry/of retry-name (.build retry-config))))

Returns a function accepting the same arguments as f but retrying on error as specified by retry. -The calling thread is blocked during the retries.

+The calling thread is blocked during the retries. This function should be used to +trigger retries across the BE, but keep in mind to not chain retries with this function.

(defn decorate
-  [f ^Retry retry]
-  (fn [& args]
-    (let [callable (reify Callable (call [_] (apply f args)))]
-      (.call (Retry/decorateCallable retry callable)))))
 

metabase.util.schema

toc

Various schemas that are useful throughout the app.

+ ([f] + (decorate f (random-exponential-backoff-retry (str (random-uuid)) (retry-configuration)))) + ([f ^Retry retry] + (fn [& args] + (let [callable (reify Callable (call [_] (apply f args)))] + (.call (Retry/decorateCallable retry callable)))))) 

metabase.util.schema

toc

Various schemas that are useful throughout the app.

Schemas defined are deprecated and should be replaced with Malli schema defined in [[metabase.util.malli.schema]]. If you update schemas in this ns, please make sure you update the malli schema too. It'll help us makes the transition easier.