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

Wire up new user notification email :e-mail:

parent 18b23dc0
No related branches found
No related tags found
No related merge requests found
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
(pre-update 1) (pre-update 1)
(project 1) (project 1)
(qp-expect-with-engines 1) (qp-expect-with-engines 1)
(render-file 1)
(resolve-private-fns 1) (resolve-private-fns 1)
(select 1) (select 1)
(sync-in-context 2) (sync-in-context 2)
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
[metabase.db :as db] [metabase.db :as db]
[metabase.email.messages :as email] [metabase.email.messages :as email]
[metabase.events :as events] [metabase.events :as events]
(metabase.models [user :refer [User set-user-password! set-user-password-reset-token!]] (metabase.models [user :refer [User set-user-password! set-user-password-reset-token!], :as user]
[session :refer [Session]] [session :refer [Session]]
[setting :refer [defsetting], :as setting]) [setting :refer [defsetting], :as setting])
[metabase.util :as u] [metabase.util :as u]
...@@ -164,14 +164,9 @@ ...@@ -164,14 +164,9 @@
(defn- google-auth-create-new-user! [first-name last-name email] (defn- google-auth-create-new-user! [first-name last-name email]
(check-autocreate-user-allowed-for-email email) (check-autocreate-user-allowed-for-email email)
(db/insert! User ;; this will just give the user a random password; they can go reset it if they ever change their mind and want to log in without Google Auth;
:first_name first-name ;; this lets us keep the NOT NULL constraints on password / salt without having to make things hairy and only enforce those for non-Google Auth users
:last_name last-name (user/create-user! first-name last-name email, :send-welcome false, :google-auth? true))
:email email
:google_auth true
;; just give the user a random password; they can go reset it if they ever change their mind and want to log in without Google Auth;
;; this lets us keep the NOT NULL constraints on password / salt without having to make things hairy and only enforce those for non-Google Auth users
:password (str (java.util.UUID/randomUUID))))
(defn- google-auth-fetch-or-create-user! [first-name last-name email] (defn- google-auth-fetch-or-create-user! [first-name last-name email]
(if-let [user (or (db/select-one [User :id :last_login] :email email) (if-let [user (or (db/select-one [User :id :last_login] :email email)
......
(ns metabase.email (ns metabase.email
(:require [clojure.string :as s] (:require [clojure.string :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[postal.core :as postal] (postal [core :as postal]
[postal.support :refer [make-props]] [support :refer [make-props]])
[metabase.models.setting :refer [defsetting] :as setting] [metabase.models.setting :refer [defsetting] :as setting]
[metabase.util :as u]) [metabase.util :as u])
(:import [javax.mail Session])) (:import javax.mail.Session))
;; ## CONFIG ;; ## CONFIG
...@@ -31,12 +31,15 @@ ...@@ -31,12 +31,15 @@
(defn send-message (defn send-message
"Send an email to one or more RECIPIENTS. "Send an email to one or more RECIPIENTS.
RECIPIENTS is a sequence of email addresses; MESSAGE-TYPE must be either `:text` or `:html`. RECIPIENTS is a sequence of email addresses; MESSAGE-TYPE must be either `:text` or `:html`.
(email/send-message (email/send-message
:subject \"[Metabase] Password Reset Request\" :subject \"[Metabase] Password Reset Request\"
:recipients [\"cam@metabase.com\"] :recipients [\"cam@metabase.com\"]
:message-type :text :message-type :text
:message \"How are you today?\") :message \"How are you today?\")
Upon success, this returns the MESSAGE that was just sent." Upon success, this returns the MESSAGE that was just sent."
{:style/indent 0}
[& {:keys [subject recipients message-type message]}] [& {:keys [subject recipients message-type message]}]
{:pre [(string? subject) {:pre [(string? subject)
(sequential? recipients) (sequential? recipients)
......
(ns metabase.email.messages (ns metabase.email.messages
"Convenience functions for sending templated email messages. Each function here should represent a single email. "Convenience functions for sending templated email messages. Each function here should represent a single email.
NOTE: we want to keep this about email formatting, so don't put heavy logic here RE: building data for emails." NOTE: we want to keep this about email formatting, so don't put heavy logic here RE: building data for emails."
(:require [hiccup.core :refer [html]] (:require [clojure.core.cache :as cache]
[hiccup.core :refer [html]]
[medley.core :as m] [medley.core :as m]
[stencil.core :as stencil] (stencil [core :as stencil]
[metabase.email :as email] [loader :as stencil-loader])
(metabase [config :as config]
[email :as email])
[metabase.models.setting :as setting] [metabase.models.setting :as setting]
[metabase.pulse.render :as render] [metabase.pulse.render :as render]
[metabase.util :as u] [metabase.util :as u]
(metabase.util [quotation :as quotation] (metabase.util [quotation :as quotation]
[urls :as url]))) [urls :as url])))
;; NOTE: uncomment this in development to disable template caching ;; Dev only -- disable template caching
;; (stencil.loader/set-cache (clojure.core.cache/ttl-cache-factory {} :ttl 0)) (when config/is-dev?
(stencil-loader/set-cache (cache/ttl-cache-factory {} :ttl 0)))
;;; ### Public Interface ;;; ### Public Interface
(defn send-new-user-email (defn send-new-user-email
"Format and Send an welcome email for newly created users." "Format and send an welcome email for newly created users."
[invited invitor join-url] [invited invitor join-url]
(let [data-quote (quotation/random-quote) (let [data-quote (quotation/random-quote)
company (or (setting/get :site-name) "Unknown") company (or (setting/get :site-name) "Unknown")
message-body (stencil/render-file "metabase/email/new_user_invite" message-body (stencil/render-file "metabase/email/new_user_invite"
{:emailType "new_user_invite" {:emailType "new_user_invite"
:invitedName (:first_name invited) :invitedName (:first_name invited)
:invitorName (:first_name invitor) :invitorName (:first_name invitor)
:invitorEmail (:email invitor) :invitorEmail (:email invitor)
:company company :company company
:joinUrl join-url :joinUrl join-url
:quotation (:quote data-quote) :quotation (:quote data-quote)
:quotationAuthor (:author data-quote) :quotationAuthor (:author data-quote)
:today (u/format-date "MMM' 'dd,' 'yyyy" (System/currentTimeMillis)) :today (u/format-date "MMM' 'dd,' 'yyyy")
:logoHeader true})] :logoHeader true})]
(email/send-message
:subject (str "You're invited to join " company "'s Metabase")
:recipients [(:email invited)]
:message-type :html
:message message-body)))
(defn send-user-joined-admin-notification-email
"Send an email to the admin of this Metabase instance letting them know a new user joined."
[new-user invitor google-auth?]
{:pre [(map? new-user)
(m/boolean? google-auth?)
(or google-auth?
(and (map? invitor)
(u/is-email? (:email invitor))))]}
(let [data-quote (quotation/random-quote)]
(email/send-message (email/send-message
:subject (str "You're invited to join " company "'s Metabase") :subject (format (if google-auth?
:recipients [(:email invited)] "%s created a Metabase account"
:message-type :html "%s accepted your Metabase invite")
:message message-body))) (:common_name new-user))
:recipients [(if google-auth?
(setting/get :admin-email)
(:email invitor))]
:message-type :html
:message (stencil/render-file "metabase/email/user_joined_notification"
{:logoHeader true
:quotation (:quote data-quote)
:quotationAuthor (:author data-quote)
:joinedUserName (:first_name new-user)
:joinedViaSSO google-auth?
:joinedUserEmail (:email new-user)
:joinedDate (u/format-date "hh:mm") ; TODO - is this what we want?
:invitorEmail (:email invitor)
:joinedUserEditUrl (str (setting/get :-site-url) "/admin/people")}))))
(defn send-password-reset-email (defn send-password-reset-email
"Format and Send an email informing the user how to reset their password." "Format and send an email informing the user how to reset their password."
[email google-auth? hostname password-reset-url] [email google-auth? hostname password-reset-url]
{:pre [(string? email) {:pre [(string? email)
(m/boolean? google-auth?) (m/boolean? google-auth?)
...@@ -47,19 +83,20 @@ ...@@ -47,19 +83,20 @@
(string? hostname) (string? hostname)
(string? password-reset-url)]} (string? password-reset-url)]}
(let [message-body (stencil/render-file "metabase/email/password_reset" (let [message-body (stencil/render-file "metabase/email/password_reset"
{:emailType "password_reset" {:emailType "password_reset"
:hostname hostname :hostname hostname
:sso google-auth? :sso google-auth?
:passwordResetUrl password-reset-url :passwordResetUrl password-reset-url
:logoHeader true})] :logoHeader true})]
(email/send-message (email/send-message
:subject "[Metabase] Password Reset Request" :subject "[Metabase] Password Reset Request"
:recipients [email] :recipients [email]
:message-type :html :message-type :html
:message message-body))) :message message-body)))
(defn send-notification-email (defn send-notification-email
"Format and Send an email informing the user about changes to objects in the system." "Format and send an email informing the user about changes to objects in the system."
[email context] [email context]
{:pre [(string? email) {:pre [(string? email)
(u/is-email? email) (u/is-email? email)
...@@ -91,8 +128,9 @@ ...@@ -91,8 +128,9 @@
:message-type :html :message-type :html
:message message-body))) :message message-body)))
(defn send-follow-up-email (defn send-follow-up-email
"Format and Send an email to the system admin following up on the installation." "Format and send an email to the system admin following up on the installation."
[email msg-type] [email msg-type]
{:pre [(string? email) {:pre [(string? email)
(u/is-email? email) (u/is-email? email)
...@@ -119,6 +157,7 @@ ...@@ -119,6 +157,7 @@
:message-type :html :message-type :html
:message message-body))) :message message-body)))
;; HACK: temporary workaround to postal requiring a file as the attachment ;; HACK: temporary workaround to postal requiring a file as the attachment
(defn- write-byte-array-to-temp-file (defn- write-byte-array-to-temp-file
[^bytes img-bytes] [^bytes img-bytes]
...@@ -145,14 +184,14 @@ ...@@ -145,14 +184,14 @@
(render/render-pulse-section result))))) (render/render-pulse-section result)))))
data-quote (quotation/random-quote) data-quote (quotation/random-quote)
message-body (stencil/render-file "metabase/email/pulse" message-body (stencil/render-file "metabase/email/pulse"
{:emailType "pulse" {:emailType "pulse"
:pulse (html body) :pulse (html body)
:pulseName (:name pulse) :pulseName (:name pulse)
:sectionStyle render/section-style :sectionStyle render/section-style
:colorGrey4 render/color-gray-4 :colorGrey4 render/color-gray-4
:quotation (:quote data-quote) :quotation (:quote data-quote)
:quotationAuthor (:author data-quote) :quotationAuthor (:author data-quote)
:logoFooter true})] :logoFooter true})]
(apply vector {:type "text/html; charset=utf-8" :content message-body} (apply vector {:type "text/html; charset=utf-8" :content message-body}
(map-indexed (fn [idx bytes] {:type :inline (map-indexed (fn [idx bytes] {:type :inline
:content-id (str "IMAGE" idx) :content-id (str "IMAGE" idx)
......
...@@ -3,24 +3,24 @@ ...@@ -3,24 +3,24 @@
<div style="padding-bottom: 1em;"> <div style="padding-bottom: 1em;">
<h2 style="font-weight: normal; color: #4C545B;line-height: 1.65rem;"> <h2 style="font-weight: normal; color: #4C545B;line-height: 1.65rem;">
{{joinedUserName}} {{joinedUserName}}
{{#jonedViaSSO}}created a Metabase account.{{/joinedViaSSO}} {{#joinedViaSSO}}created a Metabase account.{{/joinedViaSSO}}
{{^joinedViaSSO}}accepted your Metabase invitation.{{/joinedViaSSO}} {{^joinedViaSSO}}accepted your Metabase invitation.{{/joinedViaSSO}}
</h2> </h2>
<h4 style="font-weight: normal;"> <h4 style="font-weight: normal;">
<a style="color: #4A90E2; text-decoration: none;" href="mailto:{{invitorEmail}}"> <a style="color: #4A90E2; text-decoration: none;" href="mailto:{{invitorEmail}}">
{{joinedUserEmail}} {{joinedUserEmail}}
</a> </a>
joined {{#sso}}via Google Auth{{/sso}} at {{joinedDate}} joined {{#joinedViaSSO}}via Google Auth{{/joinedViaSSO}} at {{joinedDate}}
</h4> </h4>
</div> </div>
<div style="padding: 1em;"> <div style="padding: 1em;">
<a style="display: inline-block; box-sizing: border-box; text-decoration: none; font-size: 1.063rem; padding: 0.5rem 1.375rem; background: #FBFCFD; border: 1px solid #ddd; color: #444; cursor: pointer; text-decoration: none; font-weight: bold; border-radius: 4px; background-color: #4990E2; border-color: #4990E2; color: #fff;" href="{{joinedUserEditUrl}}"> <a style="display: inline-block; box-sizing: border-box; text-decoration: none; font-size: 1.063rem; padding: 0.5rem 1.375rem; background: #FBFCFD; border: 1px solid #ddd; color: #444; cursor: pointer; text-decoration: none; font-weight: bold; border-radius: 4px; background-color: #4990E2; border-color: #4990E2; color: #fff;" href="{{joinedUserEditUrl}}">
Edit {{joinUserName}}'s settings Edit {{joinedUserName}}'s settings
</a> </a>
</div> </div>
<div style="padding-bottom: 2em; font-size: x-small;"> <div style="padding-bottom: 2em; font-size: x-small;">
Button not working? Paste this link into your browser:<br/> Button not working? Paste this link into your browser:<br/>
{{joinUrl}} {{joinedUserEditUrl}}
</div> </div>
</div> </div>
{{> metabase/email/_footer }} {{> metabase/email/_footer }}
...@@ -70,27 +70,28 @@ ...@@ -70,27 +70,28 @@
(declare form-password-reset-url (declare form-password-reset-url
set-user-password-reset-token!) set-user-password-reset-token!)
;; TODO - `:send-welcome?` instead of `:send-welcome`
(defn create-user! (defn create-user!
"Convenience function for creating a new `User` and sending out the welcome email." "Convenience function for creating a new `User` and sending out the welcome email."
[first-name last-name email-address & {:keys [send-welcome invitor password] [first-name last-name email-address & {:keys [send-welcome invitor password google-auth?]
:or {send-welcome false}}] :or {send-welcome false
{:pre [(string? first-name) google-auth? false}}]
(string? last-name) {:pre [(string? first-name) (string? last-name) (string? email-address)]}
(string? email-address)]} (u/prog1 (db/insert! User
(when-let [new-user (db/insert! User :email email-address
:email email-address :first_name first-name
:first_name first-name :last_name last-name
:last_name last-name :password (if-not (nil? password)
:password (if-not (nil? password) password
password (str (java.util.UUID/randomUUID)))
(str (java.util.UUID/randomUUID))))] :google_auth google-auth?)
(when send-welcome (when send-welcome
(let [reset-token (set-user-password-reset-token! (:id new-user)) (let [reset-token (set-user-password-reset-token! (:id <>))
;; NOTE: the new user join url is just a password reset with an indicator that this is a first time user ;; the new user join url is just a password reset with an indicator that this is a first time user
join-url (str (form-password-reset-url reset-token) "#new")] join-url (str (form-password-reset-url reset-token) "#new")]
(email/send-new-user-email new-user invitor join-url))) (email/send-new-user-email <> invitor join-url)))
;; return the newly created user ;; notifiy the admin of this MB instance that a new user has joined (TODO - are there cases where we *don't* want to do this)
new-user)) (email/send-user-joined-admin-notification-email <> invitor google-auth?)))
(defn set-user-password! (defn set-user-password!
"Updates the stored password for a specified `User` by hashing the password with a random salt." "Updates the stored password for a specified `User` by hashing the password with a random salt."
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment