stages/email: start rewriting templates, add template tags to embed CSS and images
This commit is contained in:
parent
e989c61793
commit
a67c53f46a
|
@ -1,77 +0,0 @@
|
||||||
{% extends "email/base.html" %}
|
|
||||||
|
|
||||||
{% load utils %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block pre_header %}
|
|
||||||
{% trans "Looks like you tried signing in a few too many times. Let's see if we can get you back into your account." %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<!-- HERO -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#7c72dc" align="center" style="padding: 0px 10px 0px 10px;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="600" class="wrapper">
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
|
|
||||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">{% trans 'Trouble signing in?' %}</h1>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- COPY BLOCK -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="600" class="wrapper">
|
|
||||||
<!-- COPY -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
|
|
||||||
<p style="margin: 0;">{% trans "Resetting your password is easy. Just press the button below and follow the instructions. We'll have you up and running in no time." %}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- BULLETPROOF BUTTON -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#ffffff" align="left">
|
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="border-radius: 3px;" bgcolor="#7c72dc"><a href="{{ url }}" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #7c72dc; display: inline-block;">{% trans 'Reset Password' %}</a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- COPY CALLOUT -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="600" class="wrapper">
|
|
||||||
<!-- HEADLINE -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#111111" align="left" style="padding: 40px 30px 20px 30px; color: #ffffff; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
|
|
||||||
<h2 style="font-size: 24px; font-weight: 400; margin: 0;">{% trans 'Want a more secure account?' %}</h2>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- COPY -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#111111" align="left" style="padding: 0px 30px 20px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
|
|
||||||
<p style="margin: 0;">{% trans 'We support two-factor authentication to help keep your information private.' %}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- COPY -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#111111" align="left" style="padding: 0px 30px 40px 30px; border-radius: 0px 0px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
|
|
||||||
<p style="margin: 0;"><a href="http://litmus.com" target="_blank" style="color: #7c72dc;">{% trans 'See how easy it is to get started' %}</a></p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endblock %}
|
|
|
@ -1,129 +0,0 @@
|
||||||
{% load inline %}
|
|
||||||
{% load utils %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>passbook</title>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<style type="text/css">
|
|
||||||
/* CLIENT-SPECIFIC STYLES */
|
|
||||||
body, table, td, a {
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-ms-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table, td {
|
|
||||||
mso-table-lspace: 0pt;
|
|
||||||
mso-table-rspace: 0pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
-ms-interpolation-mode: bicubic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RESET STYLES */
|
|
||||||
img {
|
|
||||||
border: 0;
|
|
||||||
height: auto;
|
|
||||||
line-height: 100%;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100% !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS BLUE LINKS */
|
|
||||||
a[x-apple-data-detectors] {
|
|
||||||
color: inherit !important;
|
|
||||||
text-decoration: none !important;
|
|
||||||
font-size: inherit !important;
|
|
||||||
font-family: inherit !important;
|
|
||||||
font-weight: inherit !important;
|
|
||||||
line-height: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ANDROID CENTER FIX */
|
|
||||||
div[style*="margin: 16px 0;"] {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="background-color: #1b2a32; margin: 0 !important; padding: 0 !important;">
|
|
||||||
|
|
||||||
<!-- HIDDEN PREHEADER TEXT -->
|
|
||||||
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Metropolis', Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
|
|
||||||
{% block pre_header %}
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
||||||
<!-- LOGO -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#3625b7" align="center">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="480">
|
|
||||||
<tr>
|
|
||||||
<td align="center" valign="top" style="padding: 40px 10px 40px 10px;">
|
|
||||||
<a href="" target="_blank">
|
|
||||||
<img alt="Logo" src="{% inline_static 'assets/dark.svg' %}" width="64" height="64"
|
|
||||||
style="display: block; width: 64px; max-width: 64px; min-width: 64px; font-family: 'Metropolis', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;"
|
|
||||||
border="0">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% block content %}
|
|
||||||
{% endblock %}
|
|
||||||
<!-- SUPPORT CALLOUT -->
|
|
||||||
<!-- <tr>
|
|
||||||
<td bgcolor="#1b2a32" align="center" style="padding: 30px 10px 0px 10px;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="480">
|
|
||||||
HEADLINE
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#566572" align="center" style="padding: 30px 30px 30px 30px; border-radius: 4px 4px 4px 4px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
|
|
||||||
<h2 style="font-size: 20px; font-weight: 400; color: ##E9ECEF; margin: 0;">Need more help?</h2>
|
|
||||||
<p style="margin: 0;"><a href="http://litmus.com" target="_blank" style="color: #3625b7;">We’re
|
|
||||||
here, ready to talk</a></p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr> -->
|
|
||||||
<!-- FOOTER -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#1b2a32" align="center" style="padding: 0px 10px 0px 10px;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="480">
|
|
||||||
<!-- NAVIGATION -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#1b2a32" align="left" style="padding: 30px 30px 30px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
|
|
||||||
<p style="margin: 0;">
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- ADDRESS -->
|
|
||||||
<tr>
|
|
||||||
<td bgcolor="#1b2a32" align="left" style="padding: 0px 30px 30px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
|
|
||||||
<p style="margin: 0;"><a href="passbook">passbook</a></p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,20 +0,0 @@
|
||||||
"""passbook core inlining template tags"""
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django import template
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
|
||||||
def inline_static(path):
|
|
||||||
"""Inline static asset. If file is binary, return b64 representation"""
|
|
||||||
prefix = "data:image/svg+xml;utf8,"
|
|
||||||
data = ""
|
|
||||||
full_path = settings.STATIC_ROOT + "/" + path
|
|
||||||
if os.path.exists(full_path):
|
|
||||||
if full_path.endswith(".svg"):
|
|
||||||
with open(full_path) as _file:
|
|
||||||
data = _file.read()
|
|
||||||
return prefix + data
|
|
|
@ -75,6 +75,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"django.contrib.postgres",
|
"django.contrib.postgres",
|
||||||
|
"django.contrib.humanize",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"drf_yasg",
|
"drf_yasg",
|
||||||
"guardian",
|
"guardian",
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
"""passbook multi-stage authentication engine"""
|
"""passbook multi-stage authentication engine"""
|
||||||
|
from datetime import timedelta
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Nonce
|
from passbook.core.models import Nonce
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from passbook.flows.stage import AuthenticationStage
|
from passbook.flows.stage import AuthenticationStage
|
||||||
from passbook.lib.config import CONFIG
|
|
||||||
from passbook.stages.email.tasks import send_mails
|
from passbook.stages.email.tasks import send_mails
|
||||||
from passbook.stages.email.utils import TemplateEmailMessage
|
from passbook.stages.email.utils import TemplateEmailMessage
|
||||||
|
|
||||||
|
@ -18,32 +21,40 @@ LOGGER = get_logger()
|
||||||
class EmailStageView(AuthenticationStage):
|
class EmailStageView(AuthenticationStage):
|
||||||
"""E-Mail stage which sends E-Mail for verification"""
|
"""E-Mail stage which sends E-Mail for verification"""
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
template_name = "stages/email/waiting_message.html"
|
||||||
kwargs["show_password_forget_notice"] = CONFIG.y(
|
|
||||||
"passbook.password_reset.enabled"
|
|
||||||
)
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
# TODO: Form to make sure email is only sent once
|
||||||
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
nonce = Nonce.objects.create(user=pending_user)
|
# TODO: Get expiry from Stage setting
|
||||||
|
valid_delta = timedelta(
|
||||||
|
minutes=31
|
||||||
|
) # 31 because django timesince always rounds down
|
||||||
|
nonce = Nonce.objects.create(user=pending_user, expires=now() + valid_delta)
|
||||||
# Send mail to user
|
# Send mail to user
|
||||||
message = TemplateEmailMessage(
|
message = TemplateEmailMessage(
|
||||||
subject=_("Forgotten password"),
|
subject=_("passbook - Password Recovery"),
|
||||||
template_name="email/account_password_reset.html",
|
template_name="stages/email/for_email/password_reset.html",
|
||||||
to=[pending_user.email],
|
to=[pending_user.email],
|
||||||
template_context={
|
template_context={
|
||||||
"url": self.request.build_absolute_uri(
|
"url": self.request.build_absolute_uri(
|
||||||
reverse(
|
reverse(
|
||||||
"passbook_core:auth-password-reset",
|
"passbook_flows:flow-executor",
|
||||||
kwargs={"nonce": nonce.uuid},
|
kwargs={"flow_slug": self.executor.flow.slug},
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
+ "?token="
|
||||||
|
+ quote(nonce.uuid.hex)
|
||||||
|
),
|
||||||
|
"user": pending_user,
|
||||||
|
"expires": nonce.expires,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
send_mails(self.executor.current_stage, message)
|
send_mails(self.executor.current_stage, message)
|
||||||
messages.success(request, _("Check your E-Mails for a password reset link."))
|
messages.success(request, _("Check your E-Mails for a password reset link."))
|
||||||
return self.executor.cancel()
|
# We can't call stage_ok yet, as we're still waiting
|
||||||
|
# for the user to click the link in the email
|
||||||
|
# return self.executor.stage_ok()
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request: HttpRequest):
|
def post(self, request: HttpRequest):
|
||||||
"""Just redirect to next stage"""
|
"""Just redirect to next stage"""
|
||||||
|
|
|
@ -0,0 +1,325 @@
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
|
||||||
|
/*All the styling goes here*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
|
||||||
|
.body {
|
||||||
|
background-color: #fafafa;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #06c;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #06c;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #06c;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #06c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #06c;
|
||||||
|
border-color: #06c;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #fafafa;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
#MessageViewBody a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
}
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ from smtplib import SMTPException
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from celery import group
|
from celery import group
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.root.celery import CELERY_APP
|
from passbook.root.celery import CELERY_APP
|
||||||
|
@ -12,7 +12,7 @@ from passbook.stages.email.models import EmailStage
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
def send_mails(stage: EmailStage, *messages: List[EmailMessage]):
|
def send_mails(stage: EmailStage, *messages: List[EmailMultiAlternatives]):
|
||||||
"""Wrapper to convert EmailMessage to dict and send it from worker"""
|
"""Wrapper to convert EmailMessage to dict and send it from worker"""
|
||||||
tasks = []
|
tasks = []
|
||||||
for message in messages:
|
for message in messages:
|
||||||
|
@ -22,7 +22,9 @@ def send_mails(stage: EmailStage, *messages: List[EmailMessage]):
|
||||||
return promise
|
return promise
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True)
|
@CELERY_APP.task(
|
||||||
|
bind=True, autoretry_for=(SMTPException, ConnectionError,), retry_backoff=True
|
||||||
|
)
|
||||||
def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
|
def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
|
||||||
"""Send E-Mail according to EmailStage parameters from background worker.
|
"""Send E-Mail according to EmailStage parameters from background worker.
|
||||||
Automatically retries if message couldn't be sent."""
|
Automatically retries if message couldn't be sent."""
|
||||||
|
@ -31,14 +33,11 @@ def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
|
||||||
backend.open()
|
backend.open()
|
||||||
# Since django's EmailMessage objects are not JSON serialisable,
|
# Since django's EmailMessage objects are not JSON serialisable,
|
||||||
# we need to rebuild them from a dict
|
# we need to rebuild them from a dict
|
||||||
message_object = EmailMessage()
|
message_object = EmailMultiAlternatives()
|
||||||
for key, value in message.items():
|
for key, value in message.items():
|
||||||
setattr(message_object, key, value)
|
setattr(message_object, key, value)
|
||||||
message_object.from_email = stage.from_address
|
message_object.from_email = stage.from_address
|
||||||
LOGGER.debug("Sending mail", to=message_object.to)
|
LOGGER.debug("Sending mail", to=message_object.to)
|
||||||
try:
|
|
||||||
num_sent = stage.backend.send_messages([message_object])
|
num_sent = stage.backend.send_messages([message_object])
|
||||||
except SMTPException as exc:
|
|
||||||
raise self.retry(exc=exc)
|
|
||||||
if num_sent != 1:
|
if num_sent != 1:
|
||||||
raise self.retry()
|
raise self.retry()
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
{% load passbook_stages_email %}
|
||||||
|
{% load utils %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Simple Transactional Email</title>
|
||||||
|
<style>{% inline_static_ascii "stages/email/css/base.css" %}</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="">
|
||||||
|
<span class="preheader">
|
||||||
|
{% block pre_header %}
|
||||||
|
{% endblock %}
|
||||||
|
</span>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<table role="presentation" class="main">
|
||||||
|
<img src="{% inline_static_binary "passbook/logo.svg" %}" alt="">
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by">
|
||||||
|
Powered by <a href="https://beryju.github.io/passbook/">passbook</a>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends "stages/email/for_email/base.html" %}
|
||||||
|
|
||||||
|
{% load utils %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<td>
|
||||||
|
<h2>
|
||||||
|
{% blocktrans with username=user.username %}
|
||||||
|
Hi {{ username }},
|
||||||
|
{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}
|
||||||
|
You recently requested to change your password for you passbook account. Use the button below to set a new password.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{{ url }}" target="_blank">{% trans 'Reset Password' %}</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
{% blocktrans with expires=expires|naturaltime %}
|
||||||
|
If you did not request a password change, please ignore this E-Mail. The link above is valid for {{ expires }}.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1 @@
|
||||||
|
check your emails mate
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""passbook core inlining template tags"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def inline_static_ascii(path: str) -> Optional[str]:
|
||||||
|
"""Inline static asset. Doesn't check file contents, plain text is assumed"""
|
||||||
|
result = finders.find(path)
|
||||||
|
if os.path.exists(result):
|
||||||
|
with open(result) as _file:
|
||||||
|
return _file.read()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def inline_static_binary(path: str) -> Optional[str]:
|
||||||
|
"""Inline static asset. Uses file extension for base64 block"""
|
||||||
|
result = finders.find(path)
|
||||||
|
suffix = Path(path).suffix
|
||||||
|
if os.path.exists(result):
|
||||||
|
with open(result) as _file:
|
||||||
|
return f"data:image/{suffix};base64," + _file.read()
|
||||||
|
return None
|
|
@ -8,34 +8,10 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
||||||
"""Wrapper around EmailMultiAlternatives with integrated template rendering"""
|
"""Wrapper around EmailMultiAlternatives with integrated template rendering"""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(
|
def __init__(self, template_name=None, template_context=None, **kwargs):
|
||||||
self,
|
|
||||||
subject="",
|
|
||||||
body=None,
|
|
||||||
from_email=None,
|
|
||||||
to=None,
|
|
||||||
bcc=None,
|
|
||||||
connection=None,
|
|
||||||
attachments=None,
|
|
||||||
headers=None,
|
|
||||||
cc=None,
|
|
||||||
reply_to=None,
|
|
||||||
template_name=None,
|
|
||||||
template_context=None,
|
|
||||||
):
|
|
||||||
html_content = render_to_string(template_name, template_context)
|
html_content = render_to_string(template_name, template_context)
|
||||||
if not body:
|
if "body" not in kwargs:
|
||||||
body = strip_tags(html_content)
|
kwargs["body"] = strip_tags(html_content)
|
||||||
super().__init__(
|
super().__init__(**kwargs)
|
||||||
subject=subject,
|
self.content_subtype = "html"
|
||||||
body=body,
|
|
||||||
from_email=from_email,
|
|
||||||
to=to,
|
|
||||||
bcc=bcc,
|
|
||||||
connection=connection,
|
|
||||||
attachments=attachments,
|
|
||||||
headers=headers,
|
|
||||||
cc=cc,
|
|
||||||
reply_to=reply_to,
|
|
||||||
)
|
|
||||||
self.attach_alternative(html_content, "text/html")
|
self.attach_alternative(html_content, "text/html")
|
||||||
|
|
Reference in New Issue