Email styling using email.less (#3704)
* DRY font-family declarations * Add email.less, inline styles using premailer * min-width 100% for mobile email clients * Emails without header have default 100% width (like before) * Include email.css for all apps * Keep !important declarations * Add test case for inlining css * Ignore important rules in css * minor
This commit is contained in:
parent
4f7feddb6a
commit
db22aa1c87
12 changed files with 206 additions and 46 deletions
|
|
@ -247,7 +247,27 @@ def get_formatted_html(subject, message, footer=None, print_html=None, email_acc
|
|||
"subject": subject
|
||||
})
|
||||
|
||||
return scrub_urls(rendered_email)
|
||||
sanitized_html = scrub_urls(rendered_email)
|
||||
transformed_html = inline_style_in_html(sanitized_html)
|
||||
return transformed_html
|
||||
|
||||
def inline_style_in_html(html):
|
||||
''' Convert email.css and html to inline-styled html
|
||||
'''
|
||||
from premailer import Premailer
|
||||
|
||||
apps = frappe.get_installed_apps()
|
||||
|
||||
css_files = []
|
||||
for app in apps:
|
||||
path = 'assets/{0}/css/email.css'.format(app)
|
||||
if os.path.exists(os.path.abspath(path)):
|
||||
css_files.append(path)
|
||||
|
||||
p = Premailer(html=html, external_styles=css_files, strip_important=False)
|
||||
|
||||
return p.transform()
|
||||
|
||||
|
||||
def add_attachment(fname, fcontent, content_type=None,
|
||||
parent=None, content_id=None, inline=False):
|
||||
|
|
@ -407,7 +427,6 @@ def get_header():
|
|||
else:
|
||||
email_brand_image = default_brand_image
|
||||
|
||||
email_brand_image = default_brand_image
|
||||
brand_text = frappe.get_hooks('app_title')[-1]
|
||||
|
||||
email_header, text = get_email_from_template('email_header', {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, unittest, os, base64
|
||||
from frappe.email.email_body import replace_filename_with_cid, get_email
|
||||
from frappe.email.email_body import (replace_filename_with_cid,
|
||||
get_email, inline_style_in_html)
|
||||
|
||||
class TestEmailBody(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
|
@ -95,6 +96,17 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|||
'''.format(inline_images[0].get('content_id'))
|
||||
self.assertEquals(message, processed_message)
|
||||
|
||||
def test_inline_styling(self):
|
||||
html = '''
|
||||
<h3>Hi John</h3>
|
||||
<p>This is a test email</p>
|
||||
'''
|
||||
transformed_html = '''
|
||||
<h3>Hi John</h3>
|
||||
<p style="margin:1em 0 !important">This is a test email</p>
|
||||
'''
|
||||
self.assertTrue(transformed_html in inline_style_in_html(html))
|
||||
|
||||
|
||||
def fixed_column_width(string, chunk_size):
|
||||
parts = [string[0+i:chunk_size+i] for i in range(0, len(string), chunk_size)]
|
||||
|
|
|
|||
64
frappe/public/css/email.css
Normal file
64
frappe/public/css/email.css
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/* csslint ignore:start */
|
||||
body {
|
||||
line-height: 1.5;
|
||||
color: #36414C;
|
||||
}
|
||||
p {
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
.body-table,
|
||||
.email-body tr,
|
||||
.email-footer tr {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
.email-header,
|
||||
.email-body,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
.email-body tr {
|
||||
font-size: 14px;
|
||||
}
|
||||
.email-footer {
|
||||
border-top: 1px solid #d1d8dd;
|
||||
}
|
||||
.email-footer tr {
|
||||
font-size: 12px;
|
||||
}
|
||||
.email-header {
|
||||
background: #fafbfc;
|
||||
border: 1px solid #d1d8dd;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.email-header .brand-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
.body-table.has-header .email-body {
|
||||
border: 1px solid #d1d8dd;
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: none;
|
||||
}
|
||||
.body-table.has-header .email-footer {
|
||||
border-top: none;
|
||||
}
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
border: 1px solid;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.btn.btn-default {
|
||||
color: #fff;
|
||||
background-color: #f0f4f7;
|
||||
border-color: transparent;
|
||||
}
|
||||
.btn.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #5E64FF;
|
||||
border-color: #444bff;
|
||||
}
|
||||
/* csslint ignore:end */
|
||||
|
|
@ -1,21 +1,8 @@
|
|||
@import "variables.less";
|
||||
@import "mixins.less";
|
||||
|
||||
// @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300,600,700);
|
||||
//
|
||||
// body {
|
||||
// font-family: "Open Sans", "Helvetica", Arial, "sans-serif";
|
||||
// }
|
||||
|
||||
html {
|
||||
// overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
|
||||
"Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-family: @font-stack;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ body {
|
|||
// position: relative;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
|
||||
"Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-family: @font-stack;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
78
frappe/public/less/email.less
Normal file
78
frappe/public/less/email.less
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/* csslint ignore:start */
|
||||
@import "variables.less";
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
|
||||
.body-table, .email-body tr, .email-footer tr {
|
||||
font-family: @font-stack;
|
||||
}
|
||||
|
||||
.email-header, .email-body, .email-footer {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.email-body tr {
|
||||
font-size: @text-regular;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
border-top: 1px solid @border-color;
|
||||
|
||||
tr {
|
||||
font-size: @text-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.email-header {
|
||||
background: @light-bg;
|
||||
border: 1px solid @border-color;
|
||||
border-radius: 3px 3px 0 0;
|
||||
|
||||
.brand-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.body-table.has-header {
|
||||
.email-body {
|
||||
border: 1px solid @border-color;
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
border: 1px solid;
|
||||
border-radius: 3px;
|
||||
|
||||
&.btn-default {
|
||||
color: #fff;
|
||||
background-color: #f0f4f7;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
color: #fff;
|
||||
background-color: @brand-primary;
|
||||
border-color: #444bff;
|
||||
}
|
||||
}
|
||||
|
||||
/* csslint ignore:end */
|
||||
|
|
@ -49,6 +49,10 @@
|
|||
@screen-sm: 991px;
|
||||
@screen-md: 1199px;
|
||||
|
||||
@font-stack: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
|
||||
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
|
||||
// palette colors
|
||||
@red: #FC4F51;
|
||||
@red-light: #FD8B8B;
|
||||
|
|
|
|||
|
|
@ -3,17 +3,9 @@
|
|||
@import "avatar.less";
|
||||
@import "indicator.less";
|
||||
|
||||
// html, body {
|
||||
// font-family: "Open Sans", "Helvetica Neue", Serif;
|
||||
// color: @text-light;
|
||||
// }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
|
||||
"Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
color: @text-color;
|
||||
font-family: @font-stack;
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
a& {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="email_header">
|
||||
<table class="email-header" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td width="35" height="50" style="border-bottom: 1px solid #d1d8dd;">
|
||||
<img embed="{{brand_image}}" width="24" height="24" style="display: block;" alt="{{brand_text}}">
|
||||
<td width="15"></td>
|
||||
<td width="35" height="50">
|
||||
<img class="brand-image" embed="{{brand_image}}" alt="{{brand_text}}">
|
||||
</td>
|
||||
<td style="border-bottom: 1px solid #d1d8dd;">
|
||||
<td>
|
||||
<p>{{ brand_text }}</p>
|
||||
</td>
|
||||
<td width="15"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
<h3>{{_("Password Reset")}}</h3>
|
||||
<br>
|
||||
|
||||
<p>{{_("Dear")}} {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},</p>
|
||||
<p>{{_("Please click on the following link to set your new password")}}:</p>
|
||||
<p><a href="{{ link }}">{{ link }}</a></p>
|
||||
<p>{{_("Thank you")}},<br>
|
||||
{{ user_fullname }}</p>
|
||||
<p><a class="btn btn-primary" href="{{ link }}">Reset your password</a></p>
|
||||
<p>
|
||||
{{_("Thank you")}},<br>
|
||||
{{ user_fullname }}
|
||||
</p>
|
||||
|
|
@ -7,11 +7,12 @@
|
|||
<title>{{ subject or "" }}</title>
|
||||
</head>
|
||||
|
||||
<body style="line-height: 1.5; color: #36414C;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="body_table" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px;">
|
||||
<body>
|
||||
<table class="body-table {% if header %}has-header{% endif %}" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="600" id="email_container">
|
||||
<table class="email-container" border="0" cellpadding="0" cellspacing="0"
|
||||
width="{% if header %}600{% else %}100%{% endif %}">
|
||||
<tr>
|
||||
<td valign="top">
|
||||
{{ header or "" }}
|
||||
|
|
@ -19,22 +20,23 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="email_body">
|
||||
<table class="email-body" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td width="15"></td>
|
||||
<td valign="top">
|
||||
<p>{{ content }}</p>
|
||||
<p>{{ signature }}</p>
|
||||
</td>
|
||||
<td width="15"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" id="email_footer">
|
||||
<table class="email-footer" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td valign="top" style="font-family: Helvetica, Arial, sans-serif; font-size: 11px;
|
||||
margin-bottom: 15px; border-top: 1px solid #d1d8dd;" data-email-footer="true">
|
||||
<td valign="top" data-email-footer="true">
|
||||
{{ footer }}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -41,3 +41,4 @@ oauthlib
|
|||
PyJWT
|
||||
pypdf
|
||||
openpyxl
|
||||
premailer
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue