mirror of
https://github.com/penpot/penpot.git
synced 2025-05-10 13:46:38 +02:00
🎉 Add html emails.
This commit is contained in:
parent
721879aaa8
commit
fbd6e395a4
27 changed files with 368 additions and 366 deletions
|
@ -19,8 +19,9 @@
|
||||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.9.0"}
|
io.prometheus/simpleclient_hotspot {:mvn/version "0.9.0"}
|
||||||
io.prometheus/simpleclient_httpserver {: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"}
|
expound/expound {:mvn/version "0.8.4"}
|
||||||
instaparse/instaparse {:mvn/version "1.4.10"}
|
|
||||||
com.cognitect/transit-clj {:mvn/version "1.0.324"}
|
com.cognitect/transit-clj {:mvn/version "1.0.324"}
|
||||||
|
|
||||||
io.lettuce/lettuce-core {:mvn/version "5.2.2.RELEASE"}
|
io.lettuce/lettuce-core {:mvn/version "5.2.2.RELEASE"}
|
||||||
|
|
85
backend/resources/emails/base.html
Normal file
85
backend/resources/emails/base.html
Normal 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>
|
19
backend/resources/emails/change-email/en.html
Normal file
19
backend/resources/emails/change-email/en.html
Normal 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 %}
|
1
backend/resources/emails/change-email/en.subj
Normal file
1
backend/resources/emails/change-email/en.subj
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Email change
|
|
@ -1,19 +1,13 @@
|
||||||
-- begin :subject
|
|
||||||
Email change.
|
|
||||||
-- end
|
|
||||||
|
|
||||||
-- begin :body-text
|
|
||||||
Hello {{name}}!
|
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:
|
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
|
If you received this email by mistake, please consider changing your password
|
||||||
for security reasons.
|
for security reasons.
|
||||||
|
|
||||||
Enjoy!
|
Enjoy!
|
||||||
The UXBOX team.
|
The UXBOX team.
|
||||||
-- end
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
173
backend/resources/emails/partials/inline_style.html
Normal file
173
backend/resources/emails/partials/inline_style.html
Normal 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>
|
|
@ -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>
|
|
22
backend/resources/emails/password-recovery/en.html
Normal file
22
backend/resources/emails/password-recovery/en.html
Normal 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 %}
|
1
backend/resources/emails/password-recovery/en.subj
Normal file
1
backend/resources/emails/password-recovery/en.subj
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Password reset
|
|
@ -1,18 +1,12 @@
|
||||||
-- begin :subject
|
|
||||||
Password reset.
|
|
||||||
-- end
|
|
||||||
|
|
||||||
-- begin :body-text
|
|
||||||
Hello {{name}}!
|
Hello {{name}}!
|
||||||
|
|
||||||
We received a request to reset your password. Click the link below to choose a
|
We received a request to reset your password. Click the link below to choose a
|
||||||
new one:
|
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
|
If you received this email by mistake, you can safely ignore it. Your password
|
||||||
won't be changed.
|
won't be changed.
|
||||||
|
|
||||||
Enjoy!
|
Enjoy!
|
||||||
The UXBOX team.
|
The UXBOX team.
|
||||||
-- end
|
|
20
backend/resources/emails/register/en.html
Normal file
20
backend/resources/emails/register/en.html
Normal 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 %}
|
1
backend/resources/emails/register/en.subj
Normal file
1
backend/resources/emails/register/en.subj
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Verify email.
|
|
@ -1,15 +1,9 @@
|
||||||
-- begin :subject
|
|
||||||
Verify email.
|
|
||||||
-- end
|
|
||||||
|
|
||||||
-- begin :body-text
|
|
||||||
Hello {{name}}!
|
Hello {{name}}!
|
||||||
|
|
||||||
Thanks for signing up for your UXBOX account! Please verify your email using the
|
Thanks for signing up for your UXBOX account! Please verify your email using the
|
||||||
link below adn get started building mockups and prototypes today!
|
link below adn get started building mockups and prototypes today!
|
||||||
|
|
||||||
{{ publicUri }}/#/auth/verify-token?token={{token}}
|
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||||
|
|
||||||
Enjoy!
|
Enjoy!
|
||||||
The UXBOX team.
|
The UXBOX team.
|
||||||
-- end
|
|
|
@ -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
|
|
BIN
backend/resources/public/static/images/email/instagram.png
Normal file
BIN
backend/resources/public/static/images/email/instagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 901 B |
BIN
backend/resources/public/static/images/email/taiga.png
Normal file
BIN
backend/resources/public/static/images/email/taiga.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 604 B |
BIN
backend/resources/public/static/images/email/uxbox.png
Normal file
BIN
backend/resources/public/static/images/email/uxbox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 746 B |
|
@ -32,8 +32,8 @@
|
||||||
:redis-uri "redis://redis/0"
|
:redis-uri "redis://redis/0"
|
||||||
:media-directory "resources/public/media"
|
:media-directory "resources/public/media"
|
||||||
: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"
|
||||||
|
|
||||||
:sendmail-backend "console"
|
:sendmail-backend "console"
|
||||||
:sendmail-reply-to "no-reply@example.com"
|
:sendmail-reply-to "no-reply@example.com"
|
||||||
|
|
|
@ -24,8 +24,7 @@
|
||||||
|
|
||||||
(defn default-context
|
(defn default-context
|
||||||
[]
|
[]
|
||||||
{:static media/resolve-asset
|
{:assets-uri (:assets-uri cfg/config)
|
||||||
:comment (constantly nil)
|
|
||||||
:public-uri (:public-uri cfg/config)})
|
:public-uri (:public-uri cfg/config)})
|
||||||
|
|
||||||
;; --- Public API
|
;; --- Public API
|
||||||
|
|
|
@ -12,8 +12,7 @@
|
||||||
[mount.core :as mount :refer [defstate]]
|
[mount.core :as mount :refer [defstate]]
|
||||||
[uxbox.db :as db]
|
[uxbox.db :as db]
|
||||||
[uxbox.config :as cfg]
|
[uxbox.config :as cfg]
|
||||||
[uxbox.util.migrations :as mg]
|
[uxbox.util.migrations :as mg]))
|
||||||
[uxbox.util.template :as tmpl]))
|
|
||||||
|
|
||||||
(def +migrations+
|
(def +migrations+
|
||||||
{:name "uxbox-main"
|
{:name "uxbox-main"
|
||||||
|
|
|
@ -90,7 +90,6 @@
|
||||||
(emails/send! conn emails/register
|
(emails/send! conn emails/register
|
||||||
{:to (:email profile)
|
{:to (:email profile)
|
||||||
:name (:fullname profile)
|
:name (:fullname profile)
|
||||||
:public-url (:public-uri cfg/config)
|
|
||||||
:token token})
|
:token token})
|
||||||
profile)))
|
profile)))
|
||||||
|
|
||||||
|
@ -339,7 +338,6 @@
|
||||||
(emails/send! conn emails/change-email
|
(emails/send! conn emails/change-email
|
||||||
{:to (:email profile)
|
{:to (:email profile)
|
||||||
:name (:fullname profile)
|
:name (:fullname profile)
|
||||||
:public-url (:public-uri cfg/config)
|
|
||||||
:pending-email email
|
:pending-email email
|
||||||
:token token})
|
:token token})
|
||||||
nil)))
|
nil)))
|
||||||
|
@ -430,7 +428,6 @@
|
||||||
(send-email-notification [conn profile]
|
(send-email-notification [conn profile]
|
||||||
(emails/send! conn emails/password-recovery
|
(emails/send! conn emails/password-recovery
|
||||||
{:to (:email profile)
|
{:to (:email profile)
|
||||||
:public-url (:public-uri cfg/config)
|
|
||||||
:token (:token profile)
|
:token (:token profile)
|
||||||
:name (:fullname profile)}))]
|
:name (:fullname profile)}))]
|
||||||
|
|
||||||
|
|
|
@ -52,15 +52,15 @@
|
||||||
:cron (dt/cron "1 1 */1 * * ? *")
|
:cron (dt/cron "1 1 */1 * * ? *")
|
||||||
:fn #'uxbox.tasks.gc/remove-media}])
|
:fn #'uxbox.tasks.gc/remove-media}])
|
||||||
|
|
||||||
(defstate worker
|
(defstate tasks-worker
|
||||||
:start (impl/start-worker! {:tasks tasks
|
:start (impl/start-worker! {:tasks tasks
|
||||||
:xtor scheduler})
|
:xtor scheduler})
|
||||||
:stop (impl/stop! worker))
|
:stop (impl/stop! tasks-worker))
|
||||||
|
|
||||||
(defstate scheduler-worker
|
(defstate scheduler-worker
|
||||||
:start (impl/start-scheduler-worker! {:schedule schedule
|
:start (impl/start-scheduler-worker! {:schedule schedule
|
||||||
:xtor scheduler})
|
:xtor scheduler})
|
||||||
:stop (impl/stop! worker))
|
:stop (impl/stop! scheduler-worker))
|
||||||
|
|
||||||
;; --- Public API
|
;; --- Public API
|
||||||
|
|
||||||
|
|
|
@ -9,48 +9,13 @@
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[instaparse.core :as insta]
|
|
||||||
[uxbox.common.spec :as us]
|
[uxbox.common.spec :as us]
|
||||||
[uxbox.common.exceptions :as ex]
|
[uxbox.common.exceptions :as ex]
|
||||||
[uxbox.util.template :as tmpl]))
|
[uxbox.util.template :as tmpl]))
|
||||||
|
|
||||||
;; --- Impl.
|
;; --- Impl.
|
||||||
|
|
||||||
(def ^:private grammar
|
(def ^:private email-path "emails/%(id)s/%(lang)s.%(type)s")
|
||||||
(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]))
|
|
||||||
|
|
||||||
(defn- build-base-email
|
(defn- build-base-email
|
||||||
[data context]
|
[data context]
|
||||||
|
@ -66,13 +31,28 @@
|
||||||
(:body-html data) (conj {:type "text/html"
|
(:body-html data) (conj {:type "text/html"
|
||||||
:value (:body-html data)}))})
|
: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
|
(defn- impl-build-email
|
||||||
[id context]
|
[id context]
|
||||||
(let [lang (:lang context :en)
|
(let [lang (:lang context :en)
|
||||||
path (str/format email-path {:id (name id) :lang (name lang)})]
|
subj (render-email-part :subj id context)
|
||||||
(-> (tmpl/render path context)
|
html (render-email-part :html id context)
|
||||||
(parse-template)
|
text (render-email-part :txt id context)]
|
||||||
(build-base-email context))))
|
|
||||||
|
{:subject subj
|
||||||
|
:content (cond-> []
|
||||||
|
text (conj {:type "text/plain"
|
||||||
|
:value text})
|
||||||
|
html (conj {:type "text/html"
|
||||||
|
:value html}))}))
|
||||||
|
|
||||||
;; --- Public API
|
;; --- Public API
|
||||||
|
|
||||||
|
|
|
@ -12,57 +12,24 @@
|
||||||
[clojure.walk :as walk]
|
[clojure.walk :as walk]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[uxbox.common.exceptions :as ex])
|
[selmer.parser :as sp]
|
||||||
(:import
|
[uxbox.common.exceptions :as ex]))
|
||||||
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))
|
|
||||||
|
|
||||||
|
;; (sp/cache-off!)
|
||||||
|
|
||||||
(defn render
|
(defn render
|
||||||
[path context]
|
[path context]
|
||||||
(try
|
(try
|
||||||
(let [context (adapt-context context)
|
(sp/render-file path context)
|
||||||
template (.compile +mustache-factory+ path)]
|
(catch Exception cause
|
||||||
(with-out-str
|
(ex/raise :type :internal
|
||||||
(let [scope (HashMap. ^java.util.Map (walk/stringify-keys context))]
|
:code :template-render-error
|
||||||
(.execute ^Mustache template *out* scope))))
|
:cause cause))))
|
||||||
|
|
||||||
|
(defn render-string
|
||||||
|
[content context]
|
||||||
|
(try
|
||||||
|
(sp/render content context)
|
||||||
(catch Exception cause
|
(catch Exception cause
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :template-render-error
|
:code :template-render-error
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue