0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-10 14:01:29 -05:00

🎉 Add html emails.

This commit is contained in:
Andrey Antukh 2020-06-01 13:19:35 +02:00 committed by Alonso Torres
parent 721879aaa8
commit fbd6e395a4
27 changed files with 368 additions and 366 deletions

View file

@ -19,8 +19,9 @@
io.prometheus/simpleclient_hotspot {:mvn/version "0.9.0"}
io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"}
selmer/selmer {:mvn/version "1.12.18"}
expound/expound {:mvn/version "0.8.4"}
instaparse/instaparse {:mvn/version "1.4.10"}
com.cognitect/transit-clj {:mvn/version "1.0.324"}
io.lettuce/lettuce-core {:mvn/version "5.2.2.RELEASE"}

View file

@ -0,0 +1,85 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width">
{% block head %}
<title>UXBOX Email</title>
{% endblock %}
{% include "emails/partials/inline_style.html" %}
</head>
<body bgcolor="#f6f6f6" cz-shortcut-listen="true">
<!-- body -->
<table class="body-wrap">
<tbody><tr>
<td></td>
<td class="container" bgcolor="#FFFFFF">
<!-- logo -->
<div class="logo">
<img src="{{assets-uri}}/images/email/logo.png" alt="UXBOX">
</div>
<!-- content -->
<div class="content">
<table>
<tbody><tr>
<td>
{% block content %}
{% endblock %}
</td>
</tr>
</tbody></table>
</div>
<!-- /content -->
</td>
<td></td>
</tr>
</tbody>
</table>
<!-- /body -->
<!-- footer -->
<table class="footer-wrap">
<tbody><tr>
<td></td>
<td class="container">
<!-- content -->
<div class="content">
<table>
<tbody>
<tr>
<td align="center">
<p>UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.</p>
</td>
</tr>
<tr>
<td>
<div style="text-align: center; margin: 10px 0;">
<a href="#" target="_blank"><img style="display: inline-block; width: 25px; margin: 0 15px;" src="{{assets-uri}}/images/email/uxbox.png" alt="UXBOX"></a>
<a href="#" target="_blank"><img style="display: inline-block; width: 25px; margin: 0 15px;" src="{{assets-uri}}/images/email/twitter.png" alt="TWITTER"></a>
<a href="#" target="_blank"><img style="display: inline-block; width: 25px; margin: 0 15px;" src="{{assets-uri}}/images/email/github.png" alt="GITHUB"></a>
<a href="#" target="_blank"><img style="display: inline-block; width: 25px; margin: 0 15px;" src="{{assets-uri}}/images/email/instagram.png" alt="INSTAGRAM"></a>
<a href="#" target="_blank"><img style="display: inline-block; width: 25px; margin: 0 15px;" src="{{assets-uri}}/images/email/taiga.png" alt="TAIGA"></a>
</div>
</td>
</tr>
{% comment %}
<tr>
<td align="center">
<p>Sent from UXBOX | <a href="#" target="_blank"><unsubscribe>Email preferences</unsubscribe></a>
</p>
</td>
</tr>
{% endcomment %}
</tbody>
</table>
</div><!-- /content -->
</td>
<td></td>
</tr>
</tbody>
</table>
<!-- /footer -->
</body>
</html>

View file

@ -0,0 +1,19 @@
{% extends "emails/base.html" %}
{% block content %}
<p>Hello {{name}}!</p>
<p>We received a request to change your current email to {{ pending-email }}.</p>
<p>Click to the link below to confirm the change:</p>
<a class="btn-primary" href="{{ public-uri }}/#/auth/verify-token?token={{token}}">Confirm email change</a>
<p>If you received this email by mistake, please consider changing your password
for security reasons.</p>
<p>Enjoy!</p>
<p>The UXBOX team.</p>
{% endblock %}

View file

@ -0,0 +1 @@
Email change

View file

@ -1,19 +1,13 @@
-- begin :subject
Email change.
-- end
-- begin :body-text
Hello {{name}}!
We received a request to change your current email to {{ pendingEmail }}.
We received a request to change your current email to {{ pending-email }}.
Click to the link below to confirm the change:
{{ publicUri }}/#/auth/verify-token?token={{token}}
{{ public-uri }}/#/auth/verify-token?token={{token}}
If you received this email by mistake, please consider changing your password
for security reasons.
Enjoy!
The UXBOX team.
-- end

View file

@ -1,14 +0,0 @@
<html>
<body>
<section style="font-family: Monoid, monospace; font-size: 14px;">
<h1>Available Emails:</h1>
<ul>
{{#emails}}
<li>
<a href="/debug/emails/{{ id }}">{{id}}</a>
</li>
{{/emails}}
</ul>
</section>
</body>
</html>

View file

@ -1,46 +0,0 @@
<table class="footer-wrap">
<tbody>
<tr>
<td></td>
<td class="container">
<div class="content">
<table>
<tbody>
<tr>
<td>
<div style="text-align: center;">
<a href="#" target="_blank">
<img src="{{#static}}images/email/twitter.png{{/static}}"
style="display: inline-block; width: 25px; margin-right: 5px;" />
</a>
<a href="#" target="_blank">
<img src="{{#static}}images/email/github.png{{/static}}"
style="display: inline-block; width: 25px; margin-right: 5px;" />
</a>
<a href="#" target="_blank">
<img src="{{#static}}images/email/linkedin.png{{/static}}"
style="display: inline-block; width: 25px; margin-right: 5px;" />
</a>
</div>
</td>
</tr>
{{#comment}}
<tr>
<td align="center">
<p>
<span>Sent from UXBOX | </span>
<a href="#" target="_blank">
<unsubscribe>Email preferences</unsubscribe>
</a>
</p>
</td>
</tr>
{{/comment}}
</tbody>
</table>
</div>
</td>
<td></td>
</tr>
</tbody>
</table>

View file

@ -1,6 +0,0 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta content="width=device-width" name="viewport" />
<title>title</title>
{{> inline_style }}
</head>

View file

@ -0,0 +1,173 @@
<style>
/* GLOBAL */
* {
margin:0;
padding:0;
font-family: Arial, sans-serif;
font-size: 100%;
line-height: 1.6;
}
img {
max-width: 100%;
width: 100%;
}
body {
-webkit-font-smoothing:antialiased;
-webkit-text-size-adjust:none;
width: 100%!important;
height: 100%;
}
/* ELEMENTS */
a {
color: rgb(35, 211, 161);
text-decoration:none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
.btn-primary {
text-decoration:none;
color: #000;
background-color: #31EFB8;
padding: 10px 30px;
font-weight: bold;
margin: 20px 0;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 3px;
}
.btn-secondary {
text-decoration:none;
color: #000;
background-color: #fff;
padding: 10px 30px;
font-weight: bold;
margin: 20px 0;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 3px;
border: 1px solid #000;
}
.btn-primary:hover,
.btn-secondary:hover {
color: #31EFB8;
background-color: #000;
text-decoration: none;
}
.last {
margin-bottom: 0;
}
.first{
margin-top: 0;
}
.logo {
background-color: #f6f6f6;
padding: 10px;
}
.logo h2 {
color: #000;
font-size: 20px;
font-weight: bold;
margin-top: 15px;
}
.logo img {
max-width: 120px;
}
/* BODY */
table.body-wrap {
width: 100%;
padding: 20px;
}
table.body-wrap .container{
color: #000;
}
/* FOOTER */
table.footer-wrap {
width: 100%;
clear:both!important;
}
.footer-wrap .container p {
font-size: 12px;
color:#666666;
}
table.footer-wrap a{
color: #999;
}
/* TYPOGRAPHY */
h1,h2,h3{
font-family: Arial, sans-serif;
line-height: 1.1;
margin-bottom:15px;
color:#000;
margin: 25px 0 15px;
line-height: 1.2;
font-weight:200;
}
h1 {
color: #000;
font-size: 24px;
font-weight: bold;
}
h2 {
font-size: 22px;
}
h3 {
font-size: 18px;
}
p, ul {
margin-bottom: 20px;
font-weight: normal;
}
ul li {
margin-left:5px;
list-style-position: inside;
}
/* RESPONSIVE */
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block !important;
max-width: 620px !important;
margin: 0 auto !important; /* makes it centered */
clear: both !important;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
padding: 20px;
max-width: 620px;
margin: 0 auto;
display: block;
}
/* Let's make sure tables in the content area are 100% wide */
.content table {
width: 100%;
}
</style>

View file

@ -1,162 +0,0 @@
<style>
/* GLOBAL */
* {
margin:0;
padding:0;
font-family: Arial, sans-serif;
font-size: 100%;
line-height: 1.6;
}
img {
max-width: 100%;
width: 100%;
}
.img-header {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
body {
-webkit-font-smoothing:antialiased;
-webkit-text-size-adjust:none;
width: 100%!important;
height: 100%;
}
/* ELEMENTS */
a {
color: #78dbbe;
text-decoration:none;
font-weight: bold;
}
.btn-primary {
text-decoration:none;
color: #fff;
background-color: #78dbbe;
padding: 10px 30px;
font-weight: bold;
margin: 20px 0;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 4px;
}
.btn-primary:hover {
color: #FFF;
background-color: #8eefcf;
}
.last {
margin-bottom: 0;
}
.first{
margin-top: 0;
}
.logo {
background-color: #f6f6f6;
padding: 10px;
text-align: center;
padding-bottom: 25px;
}
.logo h2 {
color: #777;
font-size: 20px;
font-weight: bold;
margin-top: 15px;
}
.logo img {
max-width: 150px;
}
/* BODY */
table.body-wrap {
width: 100%;
padding: 20px;
}
table.body-wrap .container{
border-radius: 5px;
color: #ababab;
}
/* FOOTER */
table.footer-wrap {
width: 100%;
clear:both!important;
}
.footer-wrap .container p {
font-size: 12px;
color:#666;
}
table.footer-wrap a{
color: #999;
}
/* TYPOGRAPHY */
h1,h2,h3{
font-family: Arial, sans-serif;
line-height: 1.1;
margin-bottom:15px;
color:#000;
margin: 40px 0 10px;
line-height: 1.2;
font-weight:200;
}
h1 {
color: #777;
font-size: 28px;
font-weight: bold;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 18px;
}
p, ul {
margin-bottom: 10px;
font-weight: normal;
}
ul li {
margin-left:5px;
list-style-position: inside;
}
/* RESPONSIVE */
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block !important;
max-width: 620px !important;
margin: 0 auto !important; /* makes it centered */
clear: both !important;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
padding: 20px;
max-width: 620px;
margin: 0 auto;
display: block;
}
/* Let's make sure tables in the content area are 100% wide */
.content table {
width: 100%;
}
</style>

View file

@ -0,0 +1,22 @@
{% extends "emails/base.html" %}
{% block content %}
<p>Hello {{name}}!</p>
<p>We received a request to reset your password. Click the link
below to choose a new one:</p>
<a class="btn-primary" href="{{ public-uri }}/#/auth/recovery?token={{token}}">
Reset password.
</a>
<p>
If you received this email by mistake, you can safely ignore
it. Your password won't be changed.
</p>
<p>Enjoy!</p>
<p>The UXBOX team.</p>
{% endblock %}

View file

@ -0,0 +1 @@
Password reset

View file

@ -1,18 +1,12 @@
-- begin :subject
Password reset.
-- end
-- begin :body-text
Hello {{name}}!
We received a request to reset your password. Click the link below to choose a
new one:
{{ publicUri }}/#/auth/recovery?token={{token}}
{{ public-uri }}/#/auth/recovery?token={{token}}
If you received this email by mistake, you can safely ignore it. Your password
won't be changed.
Enjoy!
The UXBOX team.
-- end

View file

@ -0,0 +1,20 @@
{% extends "emails/base.html" %}
{% block content %}
<p>Hello {{name}}!</p>
<p>
Thanks for signing up for your UXBOX account! Please verify your
email using the link below adn get started building mockups and
prototypes today!
</p>
<a class="btn-primary" href="{{public-uri}}/#/auth/verify-token?token={{token}}">
Verify token
</a>
<p>Enjoy!</p>
<p>The UXBOX team.</p>
{% endblock %}

View file

@ -0,0 +1 @@
Verify email.

View file

@ -1,15 +1,9 @@
-- begin :subject
Verify email.
-- end
-- begin :body-text
Hello {{name}}!
Thanks for signing up for your UXBOX account! Please verify your email using the
link below adn get started building mockups and prototypes today!
{{ publicUri }}/#/auth/verify-token?token={{token}}
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
The UXBOX team.
-- end

View file

@ -1,17 +0,0 @@
-- begin :subject
Bienvenue sur UXBOX.
-- end
-- begin :body-text
Bonjour {{user}}!
Bienvenue sur UXBOX.
L'équipe UXBOX.
-- end
-- begin :body-html
<p>Bonjour {{user}} !</p>
<p>Bienvenue sur UXBOX.</p>
<p>L'équipe UXBOX.</p>
-- end

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

View file

@ -32,8 +32,8 @@
:redis-uri "redis://redis/0"
:media-directory "resources/public/media"
:assets-directory "resources/public/static"
:media-uri "http://localhost:6060/media/"
:assets-uri "http://localhost:6060/static/"
:media-uri "http://localhost:6060/media"
:assets-uri "http://localhost:6060/static"
:sendmail-backend "console"
:sendmail-reply-to "no-reply@example.com"

View file

@ -24,8 +24,7 @@
(defn default-context
[]
{:static media/resolve-asset
:comment (constantly nil)
{:assets-uri (:assets-uri cfg/config)
:public-uri (:public-uri cfg/config)})
;; --- Public API

View file

@ -12,8 +12,7 @@
[mount.core :as mount :refer [defstate]]
[uxbox.db :as db]
[uxbox.config :as cfg]
[uxbox.util.migrations :as mg]
[uxbox.util.template :as tmpl]))
[uxbox.util.migrations :as mg]))
(def +migrations+
{:name "uxbox-main"

View file

@ -90,7 +90,6 @@
(emails/send! conn emails/register
{:to (:email profile)
:name (:fullname profile)
:public-url (:public-uri cfg/config)
:token token})
profile)))
@ -339,7 +338,6 @@
(emails/send! conn emails/change-email
{:to (:email profile)
:name (:fullname profile)
:public-url (:public-uri cfg/config)
:pending-email email
:token token})
nil)))
@ -430,7 +428,6 @@
(send-email-notification [conn profile]
(emails/send! conn emails/password-recovery
{:to (:email profile)
:public-url (:public-uri cfg/config)
:token (:token profile)
:name (:fullname profile)}))]

View file

@ -52,15 +52,15 @@
:cron (dt/cron "1 1 */1 * * ? *")
:fn #'uxbox.tasks.gc/remove-media}])
(defstate worker
(defstate tasks-worker
:start (impl/start-worker! {:tasks tasks
:xtor scheduler})
:stop (impl/stop! worker))
:stop (impl/stop! tasks-worker))
(defstate scheduler-worker
:start (impl/start-scheduler-worker! {:schedule schedule
:xtor scheduler})
:stop (impl/stop! worker))
:stop (impl/stop! scheduler-worker))
;; --- Public API

View file

@ -9,48 +9,13 @@
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[instaparse.core :as insta]
[uxbox.common.spec :as us]
[uxbox.common.exceptions :as ex]
[uxbox.util.template :as tmpl]))
;; --- Impl.
(def ^:private grammar
(str "message = part*"
"part = begin header body end; "
"header = tag* eol; "
"tag = space keyword; "
"body = line*; "
"begin = #'--\\s+begin\\s+'; "
"end = #'--\\s+end\\s*' eol*; "
"keyword = #':[\\w\\-]+'; "
"space = #'\\s*'; "
"line = #'.*\\n'; "
"eol = ('\\n' | '\\r\\n'); "))
(def ^:private parse-fn (insta/parser grammar))
(def ^:private email-path "emails/%(id)s/%(lang)s.mustache")
(defn- parse-template
[content]
(loop [state {}
parts (drop 1 (parse-fn content))]
(if-let [[_ _ header body] (first parts)]
(let [type (get-in header [1 2 1])
type (keyword (str/slice type 1))
content (apply str (map second (rest body)))]
(recur (assoc state type (str/trim content " \n"))
(rest parts)))
state)))
(s/def ::subject string?)
(s/def ::body-text string?)
(s/def ::body-html string?)
(s/def ::parsed-email
(s/keys :req-un [::subject ::body-text]
:opt-un [::body-html]))
(def ^:private email-path "emails/%(id)s/%(lang)s.%(type)s")
(defn- build-base-email
[data context]
@ -66,13 +31,28 @@
(:body-html data) (conj {:type "text/html"
:value (:body-html data)}))})
(defn- render-email-part
[type id context]
(let [lang (:lang context :en)
path (str/format email-path {:id (name id)
:lang (name lang)
:type (name type)})]
(some-> (io/resource path)
(tmpl/render context))))
(defn- impl-build-email
[id context]
(let [lang (:lang context :en)
path (str/format email-path {:id (name id) :lang (name lang)})]
(-> (tmpl/render path context)
(parse-template)
(build-base-email context))))
subj (render-email-part :subj id context)
html (render-email-part :html id context)
text (render-email-part :txt id context)]
{:subject subj
:content (cond-> []
text (conj {:type "text/plain"
:value text})
html (conj {:type "text/html"
:value html}))}))
;; --- Public API

View file

@ -12,57 +12,24 @@
[clojure.walk :as walk]
[clojure.java.io :as io]
[cuerdas.core :as str]
[uxbox.common.exceptions :as ex])
(:import
java.io.StringReader
java.util.HashMap
java.util.function.Function;
com.github.mustachejava.DefaultMustacheFactory
com.github.mustachejava.Mustache))
(def ^DefaultMustacheFactory +mustache-factory+ (DefaultMustacheFactory.))
(defn- adapt-context
[data]
(walk/postwalk (fn [x]
(cond
(instance? clojure.lang.Named x)
(str/camel (name x))
(instance? clojure.lang.MapEntry x)
x
(fn? x)
(reify Function
(apply [this content]
(try
(x content)
(catch Exception e
(log/error e "Error on executing" x)
""))))
(or (vector? x) (list? x))
(java.util.ArrayList. ^java.util.List x)
(map? x)
(java.util.HashMap. ^java.util.Map x)
(set? x)
(java.util.HashSet. ^java.util.Set x)
:else
x))
data))
[selmer.parser :as sp]
[uxbox.common.exceptions :as ex]))
;; (sp/cache-off!)
(defn render
[path context]
(try
(let [context (adapt-context context)
template (.compile +mustache-factory+ path)]
(with-out-str
(let [scope (HashMap. ^java.util.Map (walk/stringify-keys context))]
(.execute ^Mustache template *out* scope))))
(sp/render-file path context)
(catch Exception cause
(ex/raise :type :internal
:code :template-render-error
:cause cause))))
(defn render-string
[content context]
(try
(sp/render content context)
(catch Exception cause
(ex/raise :type :internal
:code :template-render-error