Skip to content
Snippets Groups Projects
Commit fe0b35da authored by Cam Saül's avatar Cam Saül
Browse files

Merge pull request #375 from metabase/smtp_emails

Use SMTP for sending email
parents 2e2855d4 5a8648e7
Branches
Tags
No related merge requests found
......@@ -26,6 +26,7 @@
[clj-time "0.9.0"] ; library for dealing with date/time
[colorize "0.1.1" :exclusions [org.clojure/clojure]] ; string output with ANSI color codes (for logging)
[com.cemerick/friend "0.2.1"] ; auth library
[com.draines/postal "1.11.3"] ; SMTP library
[com.h2database/h2 "1.4.186"] ; embedded SQL database
[com.mattbertolini/liquibase-slf4j "1.2.1"]
[com.novemberain/monger "2.1.0"] ; MongoDB Driver
......
(ns metabase.email
(:require [clojure.data.json :as json]
[clojure.tools.logging :as log]
[clj-http.lite.client :as client]
[medley.core :as medley]
(:require [clojure.tools.logging :as log]
[postal.core :as postal]
[metabase.models.setting :refer [defsetting]]
[metabase.util :as u]))
(declare api-post-messages-send
format-recipients)
;; ## CONFIG
(defsetting mandrill-api-key "API key for Mandrill.")
(defsetting email-from-address "Email address used as the sender of system notifications." "notifications@metabase.com")
(defsetting email-from-name "Name used as the sender of the system notifications." "Metabase")
(defsetting email-smtp-host "SMTP host." "smtp.mandrillapp.com")
(defsetting email-smtp-username "SMTP username.")
(defsetting email-smtp-password "SMTP password.")
(defsetting email-smtp-port "SMTP port." "587")
;; ## PUBLIC INTERFACE
(defn send-message [subject recipients message-type message & {:as kwargs}]
{:pre [(string? subject)
(map? recipients)
(contains? #{:text :html} message-type)
(string? message)]}
(medley/mapply api-post-messages-send (merge {:subject subject
:to (format-recipients recipients)
message-type message}
kwargs)))
;; ## IMPLEMENTATION
(def ^:dynamic *send-email-fn*
"Internal function used to send messages. Should take 2 args - a map of SMTP credentials, and a map of email details.
Provided so you can swap this out with an \"inbox\" for test purposes."
postal/send-message)
(def ^:private api-prefix
"URL prefix for API calls to the Mandrill API."
"https://mandrillapp.com/api/1.0/")
(defn send-message
"Send an email to one or more RECIPIENTS.
RECIPIENTS is a sequence of email addresses; MESSAGE-TYPE must be either `:text` or `:html`.
(defn- api-post
"Make a `POST` call to the Mandrill API.
(email/send-message
:subject \"[Metabase] Password Reset Request\"
:recipients [\"cam@metabase.com\"]
:message-type :text
:message \"How are you today?\")
(api-post \"messages/send\" :body { ... })"
[endpoint & {:keys [body] :as request-map
:or {body {}}}]
{:pre [(string? endpoint)]}
(if-not (mandrill-api-key)
(log/warn "Cannot send email: no Mandrill API key!")
(let [defaults {:content-type :json
:accept :json}
body (-> body
(assoc :key (mandrill-api-key))
json/write-str)]
(client/post (str api-prefix endpoint ".json")
(merge defaults request-map {:body body})))))
(defn- api-post-messages-send
"Make a `POST messages/send` call to the Mandrill API."
[& {:as kwargs}]
(let [defaults {:from_email (email-from-address)
:from_name (email-from-name)}]
(= (:status (api-post "messages/send"
:body {:message (merge defaults kwargs)}))
200)))
(defn- format-recipients
"Format a map of email -> name in the format expected by the Mandrill API.
(format-recipients {\"cam@metabase.com\" \"Cam Saul\"})
-> {:email \"cam@metabase.com\"
:name \"Cam Saul\"
:type :to}"
[email->name]
(map (fn [[email name]]
{:pre [(u/is-email? email)
(string? name)]}
{:email email
:name name
:type :to})
email->name))
Upon success, this returns the MESSAGE that was just sent."
[& {:keys [subject recipients message-type message]}]
{:pre [(string? subject)
(sequential? recipients)
(every? u/is-email? recipients)
(contains? #{:text :html} message-type)
(string? message)]}
(try
;; Check to make sure all valid settings are set!
(when-not (email-smtp-username)
(throw (Exception. "SMTP username is not set.")))
(when-not (email-smtp-password)
(throw (Exception. "SMTP password is not set.")))
;; Now send the email
(let [{error :error error-message :message} (*send-email-fn* {:host (email-smtp-host)
:user (email-smtp-username)
:pass (email-smtp-password)
:port (Integer/parseInt (email-smtp-port))}
{:from (email-from-address)
:to recipients
:subject subject
:body (condp = message-type
:text message
:html [{:type "text/html; charset=utf-8"
:content message}])})]
(when-not (= error :SUCCESS)
(throw (Exception. (format "Emails failed to send: error: %s; message: %s" error error-message))))
message)
(catch Throwable e
(log/warn "Failed to send email: " (.getMessage e)))))
......@@ -21,11 +21,10 @@
[:p "Your account is setup and ready to go, you just need to set a password so you can login. Follow the link below to reset your account password."]
[:p [:a {:href password-reset-url} password-reset-url]]]])]
(email/send-message
"Your new Metabase account is all set up"
{email email}
:html message-body)
;; return the message body we sent
message-body))
:subject "Your new Metabase account is all set up"
:recipients [email]
:message-type :html
:message message-body)))
(defn send-password-reset-email
"Format and Send an email informing the user how to reset their password."
......@@ -40,8 +39,7 @@
"It can be safely ignored if you did not request a password reset. Click the link below to reset your password.")]
[:p [:a {:href password-reset-url} password-reset-url]]]])]
(email/send-message
"[Metabase] Password Reset Request"
{email email}
:html message-body)
;; return the message body we sent
message-body))
:subject "[Metabase] Password Reset Request"
:recipients [email]
:message-type :html
:message message-body)))
(ns metabase.email.messages-test
(:require [expectations :refer :all]
[metabase.email :as email]
[metabase.email.messages :refer :all]))
(def ^:private inbox
"Map of email addresses -> sequence of messages they've recieved."
(atom {}))
(defn- reset-inbox!
"Clear all messages from `inbox`."
[]
(reset! inbox {}))
(defn- fake-inbox-email-fn
"A function that can be used in place of `*send-email-fn*`.
Put all messages into `inbox` instead of actually sending them."
[_ email]
(doseq [recipient (:to email)]
(swap! inbox assoc recipient (-> (get @inbox recipient [])
(conj email)))))
(defmacro with-fake-inbox
"Clear `inbox`, bind `*send-email-fn*` to `fake-inbox-email-fn`, set temporary settings for `email-smtp-username`
and `email-smtp-password`, and execute BODY."
[& body]
`(binding [email/*send-email-fn* fake-inbox-email-fn]
(reset-inbox!)
;; Push some fake settings for SMTP username + password, and restore originals when done
(let [orig-username# (email/email-smtp-username)
orig-password# (email/email-smtp-password)]
(email/email-smtp-username "fake_smtp_username")
(email/email-smtp-password "ABCD1234!!")
(try ~@body
(finally (email/email-smtp-username orig-username#)
(email/email-smtp-password orig-password#))))))
;; new user email
(expect
(str "<html><body><p>Welcome to Metabase test!</p>"
"<p>Your account is setup and ready to go, you just need to set a password so you can login. "
"Follow the link below to reset your account password.</p>"
"<p><a href=\"http://localhost/some/url\">http://localhost/some/url</a></p></body></html>")
(send-new-user-email "test" "test@test.com" "http://localhost/some/url"))
[{:from "notifications@metabase.com",
:to ["test@test.com"],
:subject "Your new Metabase account is all set up",
:body [{:type "text/html; charset=utf-8",
:content (str "<html><body><p>Welcome to Metabase test!</p>"
"<p>Your account is setup and ready to go, you just need to set a password so you can login. "
"Follow the link below to reset your account password.</p>"
"<p><a href=\"http://localhost/some/url\">http://localhost/some/url</a></p></body></html>")}]}]
(with-fake-inbox
(send-new-user-email "test" "test@test.com" "http://localhost/some/url")
(@inbox "test@test.com")))
;; password reset email
(expect
(str "<html><body><p>You're receiving this e-mail because you or someone else has requested a password for your user account at test.domain.com. "
"It can be safely ignored if you did not request a password reset. Click the link below to reset your password.</p>"
"<p><a href=\"http://localhost/some/url\">http://localhost/some/url</a></p></body></html>")
(send-password-reset-email "test@test.com" "test.domain.com" "http://localhost/some/url"))
[{:from "notifications@metabase.com",
:to ["test@test.com"],
:subject "[Metabase] Password Reset Request",
:body [{:type "text/html; charset=utf-8",
:content (str "<html><body><p>You're receiving this e-mail because you or someone else has requested a password for your user account at test.domain.com. "
"It can be safely ignored if you did not request a password reset. Click the link below to reset your password.</p>"
"<p><a href=\"http://localhost/some/url\">http://localhost/some/url</a></p></body></html>")}]}]
(with-fake-inbox
(send-password-reset-email "test@test.com" "test.domain.com" "http://localhost/some/url")
(@inbox "test@test.com")))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment