♻️ Refactor email sending.

This commit is contained in:
Andrey Antukh 2020-04-20 13:44:42 +02:00
parent 6ba3a28143
commit 7fba483bf1
10 changed files with 87 additions and 68 deletions

View file

@ -4,8 +4,7 @@
"jcenter" {:url "https://jcenter.bintray.com/"}} "jcenter" {:url "https://jcenter.bintray.com/"}}
:deps :deps
{org.clojure/clojure {:mvn/version "1.10.1"} {org.clojure/clojure {:mvn/version "1.10.1"}
funcool/promesa {:mvn/version "5.1.0"} org.clojure/data.json {:mvn/version "1.0.0"}
funcool/cuerdas {:mvn/version "2020.03.26-3"}
;; Logging ;; Logging
org.clojure/tools.logging {:mvn/version "0.5.0"} org.clojure/tools.logging {:mvn/version "0.5.0"}
@ -13,7 +12,7 @@
org.apache.logging.log4j/log4j-core {:mvn/version "2.13.0"} org.apache.logging.log4j/log4j-core {:mvn/version "2.13.0"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.13.0"} org.apache.logging.log4j/log4j-jul {:mvn/version "2.13.0"}
org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.13.0"} org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.13.0"}
funcool/datoteka {:mvn/version "1.2.0"}
expound/expound {:mvn/version "0.8.4"} expound/expound {:mvn/version "0.8.4"}
instaparse/instaparse {:mvn/version "1.4.10"} instaparse/instaparse {:mvn/version "1.4.10"}
com.cognitect/transit-clj {:mvn/version "0.8.319"} com.cognitect/transit-clj {:mvn/version "0.8.319"}
@ -21,23 +20,26 @@
;; TODO: vendorize pgclient under `vertx-clojure/vertx-pgclient` ;; TODO: vendorize pgclient under `vertx-clojure/vertx-pgclient`
io.vertx/vertx-pg-client {:mvn/version "4.0.0-milestone4"} io.vertx/vertx-pg-client {:mvn/version "4.0.0-milestone4"}
io.lettuce/lettuce-core {:mvn/version "5.2.2.RELEASE"} io.lettuce/lettuce-core {:mvn/version "5.2.2.RELEASE"}
java-http-clj/java-http-clj {:mvn/version "0.4.1"}
vertx-clojure/vertx vertx-clojure/vertx
{:local/root "vendor/vertx" {:local/root "vendor/vertx"
:deps/manifest :pom} :deps/manifest :pom}
funcool/datoteka {:mvn/version "1.2.0"}
funcool/promesa {:mvn/version "5.1.0"}
funcool/cuerdas {:mvn/version "2020.03.26-3"}
funcool/sodi funcool/sodi
{:local/root "vendor/sodi" {:local/root "vendor/sodi"
:deps/manifest :pom} :deps/manifest :pom}
lambdaisland/uri {:mvn/version "1.1.0"} lambdaisland/uri {:mvn/version "1.1.0"
:exclusions [org.clojure/data.json]}
danlentz/clj-uuid {:mvn/version "0.1.9"} danlentz/clj-uuid {:mvn/version "0.1.9"}
org.jsoup/jsoup {:mvn/version "1.12.1"} org.jsoup/jsoup {:mvn/version "1.12.1"}
org.im4java/im4java {:mvn/version "1.4.0"} org.im4java/im4java {:mvn/version "1.4.0"}
org.lz4/lz4-java {:mvn/version "1.7.1"} org.lz4/lz4-java {:mvn/version "1.7.1"}
com.github.spullara.mustache.java/compiler {:mvn/version "0.9.6"} com.github.spullara.mustache.java/compiler {:mvn/version "0.9.6"}
commons-io/commons-io {:mvn/version "2.6"} commons-io/commons-io {:mvn/version "2.6"}
com.draines/postal {:mvn/version "2.0.3" com.draines/postal {:mvn/version "2.0.3"

View file

@ -31,8 +31,11 @@
:assets-directory "resources/public/static" :assets-directory "resources/public/static"
:media-uri "http://localhost:6060/media/" :media-uri "http://localhost:6060/media/"
:assets-uri "http://localhost:6060/static/" :assets-uri "http://localhost:6060/static/"
:email-reply-to "no-reply@nodomain.com"
:email-from "no-reply@nodomain.com" :sendmail-backend "console"
:sendmail-reply-to "no-reply@example.com"
:sendmail-from "no-reply@example.com"
:smtp-enabled false :smtp-enabled false
:allow-demo-users true :allow-demo-users true
:registration-enabled true :registration-enabled true
@ -51,8 +54,10 @@
(s/def ::assets-directory ::us/string) (s/def ::assets-directory ::us/string)
(s/def ::media-uri ::us/string) (s/def ::media-uri ::us/string)
(s/def ::media-directory ::us/string) (s/def ::media-directory ::us/string)
(s/def ::email-reply-to ::us/email) (s/def ::sendmail-backend ::us/string)
(s/def ::email-from ::us/email) (s/def ::sendmail-backend-apikey ::us/string)
(s/def ::sendmail-reply-to ::us/email)
(s/def ::sendmail-from ::us/email)
(s/def ::smtp-host ::us/string) (s/def ::smtp-host ::us/string)
(s/def ::smtp-port ::us/integer) (s/def ::smtp-port ::us/integer)
(s/def ::smtp-user (s/nilable ::us/string)) (s/def ::smtp-user (s/nilable ::us/string))
@ -76,8 +81,10 @@
::assets-uri ::assets-uri
::media-directory ::media-directory
::media-uri ::media-uri
::email-reply-to ::sendmail-reply-to
::email-from ::sendmail-from
::sendmail-backend
::sendmail-backend-apikey
::smtp-host ::smtp-host
::smtp-port ::smtp-port
::smtp-user ::smtp-user

View file

@ -34,16 +34,16 @@
(defn send! (defn send!
"Schedule the email for sending." "Schedule the email for sending."
([email context] (send! db/pool email context)) ([email context] (send! db/pool email context))
([conn email context] ([conn email-factory context]
(us/verify fn? email) (us/verify fn? email-factory)
(us/verify map? context) (us/verify map? context)
(let [defaults {:from (:email-from cfg/config) (let [defaults {:from (:sendmail-from cfg/config)
:reply-to (:email-reply-to cfg/config)} :reply-to (:sendmail-reply-to cfg/config)}
data (->> (merge defaults context) data (merge defaults context)
(email))] email (email-factory data)]
(tasks/schedule! conn {:name "sendmail" (tasks/schedule! conn {:name "sendmail"
:delay 0 :delay 0
:props data})))) :props email}))))
;; --- Emails ;; --- Emails
@ -62,4 +62,3 @@
(def password-recovery (def password-recovery
"A password recovery notification email." "A password recovery notification email."
(emails/build ::password-recovery default-context)) (emails/build ::password-recovery default-context))

View file

@ -117,8 +117,7 @@
(p/then decode-task-row) (p/then decode-task-row)
(p/then (fn [item] (p/then (fn [item]
(when item (when item
(log/info "Execute task" (:name item) (log/info "Execute task" (:name item))
"with props" (pr-str (:props item)))
(-> (p/do! (handle-task tasks item)) (-> (p/do! (handle-task tasks item))
(p/handle (fn [v e] (p/handle (fn [v e]
(if e (if e

View file

@ -5,33 +5,20 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.tasks.sendmail (ns uxbox.tasks.sendmail
"Email sending jobs."
(:require (:require
[clojure.data.json :as json]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[cuerdas.core :as str]
[postal.core :as postal]
[vertx.util :as vu]
[promesa.core :as p] [promesa.core :as p]
[uxbox.common.exceptions :as ex]
[uxbox.config :as cfg] [uxbox.config :as cfg]
[uxbox.core :refer [system]] [uxbox.util.http :as http]))
[uxbox.util.blob :as blob]))
(defn- get-smtp-config (defmulti sendmail (fn [config email] (:sendmail-backend config)))
[config]
{:host (:smtp-host config)
:port (:smtp-port config)
:user (:smtp-user config)
:pass (:smtp-password config)
:ssl (:smtp-ssl config)
:tls (:smtp-tls config)
:enabled (:smtp-enabled config)})
(defn- send-email-to-console (defmethod sendmail "console"
[email] [config email]
(let [out (with-out-str (let [out (with-out-str
(println "email console dump:") (println "email console dump:")
(println "******** start email" (:id email) "**********") (println "******** start email" (:id email) "**********")
@ -40,28 +27,41 @@
(println " reply-to: " (:reply-to email)) (println " reply-to: " (:reply-to email))
(println " subject: " (:subject email)) (println " subject: " (:subject email))
(println " content:") (println " content:")
(doseq [item (rest (:body email))] (doseq [item (:content email)]
(when (str/starts-with? (:type item) "text/plain") (when (= (:type item) "text/plain")
(println (:content item)))) (println (:value item))))
(println "******** end email "(:id email) "**********"))] (println "******** end email "(:id email) "**********"))]
(log/info out) (log/info out)))
{:error :SUCCESS}))
(defn send-email (defmethod sendmail "sendgrid"
[email] [config email]
(vu/blocking (let [apikey (:sendmail-backend-apikey config)
(let [config (get-smtp-config cfg/config) dest (mapv #(array-map :email %) (:to email))
result (if (:enabled config) params {:personalizations [{:to dest
(postal/send-message config email) :subject (:subject email)}]
(send-email-to-console email))] :from {:email (:from email)}
(when (not= (:error result) :SUCCESS) :reply_to {:email (:reply-to email)}
(ex/raise :type :sendmail-error :content (:content email)}
:code :email-not-sent headers {"Authorization" (str "Bearer " apikey)
:context result)) "Content-Type" "application/json"}
nil))) body (json/write-str params)]
(-> (http/send! {:method :post
:headers headers
:uri "https://api.sendgrid.com/v3/mail/send"
:body body})
(p/handle (fn [response error]
(cond
error
(log/error "Error on sending email to sendgrid:" (pr-str error))
(= 202 (:status response))
nil
:else
(log/error "Unexpected status from sendgrid:" (pr-str response))))))))
(defn handler (defn handler
{:uxbox.tasks/name "sendmail"} {:uxbox.tasks/name "sendmail"}
[{:keys [props] :as task}] [{:keys [props] :as task}]
(send-email props)) (sendmail cfg/config props))

View file

@ -30,7 +30,7 @@
"eol = ('\\n' | '\\r\\n'); ")) "eol = ('\\n' | '\\r\\n'); "))
(def ^:private parse-fn (insta/parser grammar)) (def ^:private parse-fn (insta/parser grammar))
(def ^:private email-path "emails/%(lang)s/%(id)s.mustache") (def ^:private email-path "emails/%(id)s/%(lang)s.mustache")
(defn- parse-template (defn- parse-template
[content] [content]
@ -49,7 +49,8 @@
(s/def ::body-html string?) (s/def ::body-html string?)
(s/def ::parsed-email (s/def ::parsed-email
(s/keys :req-un [::subject ::body-html ::body-html])) (s/keys :req-un [::subject ::body-text]
:opt-un [::body-html]))
(defn- build-base-email (defn- build-base-email
[data context] [data context]
@ -59,11 +60,11 @@
:hint "Seems like the email template has invalid data." :hint "Seems like the email template has invalid data."
:contex data)) :contex data))
{:subject (:subject data) {:subject (:subject data)
:body [:alternative :content (cond-> []
{:type "text/plain; charset=utf-8" (:body-text data) (conj {:type "text/plain"
:content (:body-text data)} :value (:body-text data)})
{:type "text/html; charset=utf-8" (:body-html data) (conj {:type "text/html"
:content (:body-html data)}]}) :value (:body-html data)}))})
(defn- impl-build-email (defn- impl-build-email
[id context] [id context]
@ -102,6 +103,6 @@
:hint "seems like the template is wrong or does not exists." :hint "seems like the template is wrong or does not exists."
::id id)) ::id id))
(cond-> (assoc email :id (name id)) (cond-> (assoc email :id (name id))
(:to context) (assoc :to (:to context)) (:to context) (assoc :to [(:to context)])
(:from context) (assoc :from (:from context)) (:from context) (assoc :from (:from context))
(:reply-to context) (assoc :reply-to (:reply-to context))))))) (:reply-to context) (assoc :reply-to (:reply-to context)))))))

View file

@ -5,4 +5,15 @@
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.util.http (ns uxbox.util.http
"Http related helpers.") "Http client abstraction layer."
(:require
[promesa.core :as p]
[promesa.exec :as px]
[java-http-clj.core :as http]))
(def default-client
(delay (http/build-client {:executor @px/default-executor})))
(defn send!
[req]
(http/send-async req {:client @default-client :as :string}))