diff --git a/modules/system/ServiceProvider.php b/modules/system/ServiceProvider.php
index 05d1e9f87..fe05f1de9 100644
--- a/modules/system/ServiceProvider.php
+++ b/modules/system/ServiceProvider.php
@@ -435,6 +435,15 @@ class ServiceProvider extends ModuleServiceProvider
'permissions' => ['system.manage_mail_settings'],
'order' => 620
],
+ 'mail_brand_settings' => [
+ 'label' => 'system::lang.mail_brand.menu_label',
+ 'description' => 'system::lang.mail_brand.menu_description',
+ 'category' => SettingsManager::CATEGORY_MAIL,
+ 'icon' => 'icon-paint-brush',
+ 'url' => Backend::url('system/mailbrandsettings'),
+ 'permissions' => ['system.manage_mail_settings'],
+ 'order' => 630
+ ],
'event_logs' => [
'label' => 'system::lang.event_log.menu_label',
'description' => 'system::lang.event_log.menu_description',
diff --git a/modules/system/assets/css/mailbrandsettings/mailbrandsettings.css b/modules/system/assets/css/mailbrandsettings/mailbrandsettings.css
new file mode 100644
index 000000000..042ea750f
--- /dev/null
+++ b/modules/system/assets/css/mailbrandsettings/mailbrandsettings.css
@@ -0,0 +1,4 @@
+.field-colorpicker {
+ float: right;
+ margin-top: -10px;
+}
diff --git a/modules/system/assets/js/mailbrandsettings/mailbrandsettings.js b/modules/system/assets/js/mailbrandsettings/mailbrandsettings.js
new file mode 100644
index 000000000..5c02b5f49
--- /dev/null
+++ b/modules/system/assets/js/mailbrandsettings/mailbrandsettings.js
@@ -0,0 +1,40 @@
+
+$(document).on('change', '.field-colorpicker', function() {
+ $('#brandSettingsForm').request('onRefresh', {
+ data: { 'fields': ['_mail_preview'] }
+ })
+})
+
+function createPreviewContainer(el, content) {
+ var newiframe
+
+ // Shadow DOM ignores media queries
+ // if (document.body.attachShadow) {
+ if (false) {
+ var shadow = el.attachShadow({ mode: 'open' })
+ shadow.innerHTML = content
+ }
+ else {
+ newiframe = document.createElement('iframe')
+
+ 'srcdoc' in newiframe
+ ? newiframe.srcdoc = content
+ : newiframe.src = 'data:text/html;charset=UTF-8,' + content
+
+ var parent = el.parentNode
+ parent.replaceChild(newiframe, el)
+
+ newiframe.style.width = '100%'
+ newiframe.setAttribute('frameborder', 0)
+
+ newiframe.onload = adjustIframeHeight
+ }
+
+ function adjustIframeHeight() {
+ newiframe.style.height = '500px'
+ newiframe.style.height = 1 + (newiframe.contentWindow.document.getElementsByTagName('body')[0].scrollHeight) +'px'
+ }
+
+ $(document).render(adjustIframeHeight)
+ $(window).resize(adjustIframeHeight)
+}
diff --git a/modules/system/classes/MailManager.php b/modules/system/classes/MailManager.php
index 1801e04c9..574c90700 100644
--- a/modules/system/classes/MailManager.php
+++ b/modules/system/classes/MailManager.php
@@ -4,6 +4,7 @@ use Twig;
use Markdown;
use System\Models\MailPartial;
use System\Models\MailTemplate;
+use System\Models\MailBrandSetting;
use System\Helpers\View as ViewHelper;
use System\Classes\PluginManager;
use System\Classes\MarkupManager;
@@ -48,7 +49,12 @@ class MailManager
/**
* @var bool Internal marker for rendering mode
*/
- protected $isHtmlRenderMode;
+ protected $isHtmlRenderMode = false;
+
+ /**
+ * @var bool Internal marker for booting custom twig extensions.
+ */
+ protected $isTwigStarted = false;
/**
* This function hijacks the `addContent` method of the `October\Rain\Mail\Mailer`
@@ -66,12 +72,7 @@ class MailManager
/*
* Start twig transaction
*/
- $markupManager = MarkupManager::instance();
- $markupManager->beginTransaction();
- $markupManager->registerTokenParsers([
- new MailPartialTokenParser,
- new MailComponentTokenParser
- ]);
+ $this->startTwig();
/*
* Inject global view variables
@@ -84,55 +85,98 @@ class MailManager
/*
* Subject
*/
- $customSubject = $message->getSwiftMessage()->getSubject();
- if (empty($customSubject)) {
+ $swiftMessage = $message->getSwiftMessage();
+
+ if (empty($swiftMessage->getSubject())) {
$message->subject(Twig::parse($template->subject, $data));
}
+ if (!isset($data['subject'])) {
+ $data['subject'] = $swiftMessage->getSubject();
+ }
+
/*
* HTML contents
*/
- $html = $this->renderHtmlContents($template, $data);
+ $html = $this->renderTemplate($template, $data);
$message->setBody($html, 'text/html');
/*
* Text contents
*/
- $text = $this->renderTextContents($template, $data);
+ $text = $this->renderTextTemplate($template, $data);
$message->addPart($text, 'text/plain');
/*
* End twig transaction
*/
- $markupManager->endTransaction();
+ $this->stopTwig();
}
//
// Rendering
//
- public function renderHtmlContents($template, $data)
+ /**
+ * Render the Markdown template into HTML.
+ *
+ * @param string $content
+ * @param array $data
+ * @return string
+ */
+ public function render($content, $data = [])
+ {
+ if (!$content) {
+ return '';
+ }
+
+ $html = $this->renderTwig($content, $data);
+
+ $html = Markdown::parse($html);
+
+ return $html;
+ }
+
+ public function renderTemplate($template, $data = [])
{
$this->isHtmlRenderMode = true;
- $templateHtml = $template->content_html;
-
- $html = Twig::parse($templateHtml, $data);
- $html = Markdown::parse($html);
+ $html = $this->render($template->content_html, $data);
if ($template->layout) {
- $html = Twig::parse($template->layout->content_html, [
+ $html = $this->renderTwig($template->layout->content_html, [
'content' => $html,
- 'css' => $template->layout->content_css
+ 'css' => $template->layout->content_css,
+ 'brandCss' => MailBrandSetting::compileCss()
] + (array) $data);
}
return $html;
}
- public function renderTextContents($template, $data)
+ /**
+ * Render the Markdown template into text.
+ *
+ * @param string $view
+ * @param array $data
+ * @return string
+ */
+ public function renderText($content, $data = [])
+ {
+ if (!$content) {
+ return '';
+ }
+
+ $text = $this->renderTwig($content, $data);
+
+ $text = html_entity_decode(preg_replace("/[\r\n]{2,}/", "\n\n", $text), ENT_QUOTES, 'UTF-8');
+
+ return $text;
+ }
+
+ public function renderTextTemplate($template, $data = [])
{
$this->isHtmlRenderMode = false;
@@ -142,9 +186,10 @@ class MailManager
$templateText = $template->content_html;
}
- $text = Twig::parse($templateText, $data);
+ $text = $this->renderText($templateText, $data);
+
if ($template->layout) {
- $text = Twig::parse($template->layout->content_text, [
+ $text = $this->renderTwig($template->layout->content_text, [
'content' => $text
] + (array) $data);
}
@@ -166,7 +211,7 @@ class MailManager
}
}
- public function renderHtmlPartial($partial, $params)
+ protected function renderHtmlPartial($partial, $params)
{
$content = $partial->content_html;
@@ -174,12 +219,12 @@ class MailManager
return '';
}
- $params['body'] = Markdown::parse(array_get($params, 'body'));
+ $params['body'] = array_get($params, 'body');
- return Twig::parse($content, $params);
+ return $this->renderTwig($content, $params);
}
- public function renderTextPartial($partial, $params)
+ protected function renderTextPartial($partial, $params)
{
$content = $partial->content_text ?: $partial->content_html;
@@ -187,7 +232,53 @@ class MailManager
return '';
}
- return Twig::parse($content, $params);
+ return $this->renderTwig($content, $params);
+ }
+
+ /**
+ * Internal helper for rendering Twig
+ */
+ protected function renderTwig($content, $data = [])
+ {
+ if ($this->isTwigStarted) {
+ return Twig::parse($content, $data);
+ }
+
+ $this->startTwig();
+
+ $result = Twig::parse($content, $data);
+
+ $this->stopTwig();
+
+ return $result;
+ }
+
+ protected function startTwig()
+ {
+ if ($this->isTwigStarted) {
+ return;
+ }
+
+ $this->isTwigStarted = true;
+
+ $markupManager = MarkupManager::instance();
+ $markupManager->beginTransaction();
+ $markupManager->registerTokenParsers([
+ new MailPartialTokenParser,
+ new MailComponentTokenParser
+ ]);
+ }
+
+ protected function stopTwig()
+ {
+ if (!$this->isTwigStarted) {
+ return;
+ }
+
+ $markupManager = MarkupManager::instance();
+ $markupManager->endTransaction();
+
+ $this->isTwigStarted = false;
}
//
diff --git a/modules/system/classes/MarkupManager.php b/modules/system/classes/MarkupManager.php
index 79fc8916b..9d169f277 100644
--- a/modules/system/classes/MarkupManager.php
+++ b/modules/system/classes/MarkupManager.php
@@ -39,7 +39,7 @@ class MarkupManager
/**
* @var array Transaction based extension items
*/
- protected $tranItems;
+ protected $transactionItems;
/**
* @var bool Manager is in transaction mode
@@ -114,7 +114,7 @@ class MarkupManager
*/
public function registerExtensions($type, array $definitions)
{
- $items = $this->transactionMode ? 'tranItems' : 'items';
+ $items = $this->transactionMode ? 'transactionItems' : 'items';
if (is_null($this->$items)) {
$this->$items = [];
@@ -182,8 +182,8 @@ class MarkupManager
$results = $this->items[$type];
}
- if ($this->tranItems !== null && isset($this->tranItems[$type])) {
- $results = array_merge($results, $this->tranItems[$type]);
+ if ($this->transactionItems !== null && isset($this->transactionItems[$type])) {
+ $results = array_merge($results, $this->transactionItems[$type]);
}
return $results;
@@ -381,6 +381,6 @@ class MarkupManager
{
$this->transactionMode = false;
- $this->tranItems = null;
+ $this->transactionItems = null;
}
}
diff --git a/modules/system/controllers/MailBrandSettings.php b/modules/system/controllers/MailBrandSettings.php
new file mode 100644
index 000000000..b0539a5b6
--- /dev/null
+++ b/modules/system/controllers/MailBrandSettings.php
@@ -0,0 +1,79 @@
+pageTitle = 'Customize mail appearance';
+
+ BackendMenu::setContext('October.System', 'system', 'settings');
+ SettingsManager::setContext('October.System', 'mail_brand_settings');
+ }
+
+ public function index()
+ {
+ $this->addJs('/modules/system/assets/js/mailbrandsettings/mailbrandsettings.js', 'core');
+ $this->addCss('/modules/system/assets/css/mailbrandsettings/mailbrandsettings.css', 'core');
+
+ $setting = MailBrandSetting::instance();
+
+ if ($setting->exists) {
+ return $this->update($setting->id);
+ }
+ else {
+ return $this->create();
+ }
+ }
+
+ public function renderSampleMessage()
+ {
+ $layout = new MailLayout;
+ $layout->fillFromCode('default');
+
+ $template = new MailTemplate;
+ $template->layout = $layout;
+ $template->content_html = File::get(base_path('modules/system/models/mailbrandsetting/sample_template.htm'));
+
+ return MailManager::instance()->renderTemplate($template);
+ }
+
+ public function formCreateModelObject()
+ {
+ return MailBrandSetting::instance();
+ }
+}
diff --git a/modules/system/controllers/mailbrandsettings/_field_mail_preview.htm b/modules/system/controllers/mailbrandsettings/_field_mail_preview.htm
new file mode 100644
index 000000000..4db8ba03c
--- /dev/null
+++ b/modules/system/controllers/mailbrandsettings/_field_mail_preview.htm
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/modules/system/controllers/mailbrandsettings/config_form.yaml b/modules/system/controllers/mailbrandsettings/config_form.yaml
new file mode 100644
index 000000000..0d8bec6b8
--- /dev/null
+++ b/modules/system/controllers/mailbrandsettings/config_form.yaml
@@ -0,0 +1,15 @@
+# ===================================
+# Form Behavior Config
+# ===================================
+
+# Record name
+name: system::lang.mail_brand.menu_label
+
+# Fields are defined by extension
+form: ~/modules/system/models/mailbrandsetting/fields.yaml
+
+# Model Class name
+modelClass: System\Models\MailBrandSetting
+
+# Default redirect location
+defaultRedirect: system/themes
diff --git a/modules/system/controllers/mailbrandsettings/index.htm b/modules/system/controllers/mailbrandsettings/index.htm
new file mode 100644
index 000000000..99e4331c3
--- /dev/null
+++ b/modules/system/controllers/mailbrandsettings/index.htm
@@ -0,0 +1,68 @@
+
+
+ - = e(trans($this->pageTitle)) ?>
+
+
+
+fatalError): ?>
+
+
+
+
+
+ = $this->formRenderOutsideFields() ?>
+ = $this->formRenderPrimaryTabs() ?>
+
+
+
+
+
+
+
+
+ = $this->formRenderSecondaryTabs() ?>
+
+
+
+ = Form::open(['id' => 'brandSettingsForm', 'class'=>'layout stretch']) ?>
+ = $this->makeLayout('form-with-sidebar') ?>
+ = Form::close() ?>
+
+
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('system::lang.mail_templates.return')) ?>
+
+
diff --git a/modules/system/lang/en/lang.php b/modules/system/lang/en/lang.php
index 205845f22..6f69ca17d 100644
--- a/modules/system/lang/en/lang.php
+++ b/modules/system/lang/en/lang.php
@@ -224,6 +224,10 @@ return [
'sending' => 'Sending test message...',
'return' => 'Return to template list'
],
+ 'mail_brand' => [
+ 'menu_label' => 'Customize mail templates',
+ 'menu_description' => 'Modify the colors and appearance of mail templates.',
+ ],
'install' => [
'project_label' => 'Attach to Project',
'plugin_label' => 'Install Plugin',
diff --git a/modules/system/models/MailBrandSetting.php b/modules/system/models/MailBrandSetting.php
new file mode 100644
index 000000000..a35e0311e
--- /dev/null
+++ b/modules/system/models/MailBrandSetting.php
@@ -0,0 +1,156 @@
+ $default) {
+ $this->{$var} = $config->get('brand.mail.'.Str::studly($var), $default);
+ }
+ }
+
+ public function afterSave()
+ {
+ Cache::forget(self::CACHE_KEY);
+ }
+
+ public static function renderCss()
+ {
+ if (Cache::has(self::CACHE_KEY)) {
+ return Cache::get(self::CACHE_KEY);
+ }
+
+ try {
+ $customCss = self::compileCss();
+ Cache::forever(self::CACHE_KEY, $customCss);
+ }
+ catch (Exception $ex) {
+ $customCss = '/* ' . $ex->getMessage() . ' */';
+ }
+
+ return $customCss;
+ }
+
+ protected static function getCssVars()
+ {
+ $vars = [
+ 'body_bg' => self::BODY_BG,
+ 'content_bg' => self::WHITE_COLOR,
+ 'content_inner_bg' => self::WHITE_COLOR,
+ 'button_text_color' => self::WHITE_COLOR,
+ 'button_primary_bg' => self::PRIMARY_BG,
+ 'button_positive_bg' => self::POSITIVE_BG,
+ 'button_negative_bg' => self::NEGATIVE_BG,
+ 'header_color' => self::HEADER_COLOR,
+ 'heading_color' => self::HEADING_COLOR,
+ 'text_color' => self::TEXT_COLOR,
+ 'link_color' => self::LINK_COLOR,
+ 'footer_color' => self::FOOTER_COLOR,
+ 'body_border_color' => self::BORDER_COLOR,
+ 'subcopy_border_color' => self::BORDER_COLOR,
+ 'table_border_color' => self::BORDER_COLOR,
+ 'panel_bg' => self::BORDER_COLOR,
+ 'promotion_bg' => self::WHITE_COLOR,
+ 'promotion_border_color' => self::PROMOTION_BORDER_COLOR,
+ ];
+
+ return $vars;
+ }
+
+ protected static function makeCssVars()
+ {
+ $vars = static::getCssVars();
+
+ $result = [];
+
+ foreach ($vars as $var => $default) {
+ // panel_bg -> panel-bg
+ $cssVar = str_replace('_', '-', $var);
+
+ $result[$cssVar] = self::get($var, $default);
+ }
+
+ return $result;
+ }
+
+ public static function compileCss()
+ {
+ $parser = new Less_Parser(['compress' => true]);
+ $basePath = base_path('modules/system/models/mailbrandsetting');
+
+ $parser->ModifyVars(static::makeCssVars());
+
+ $parser->parse(File::get($basePath . '/custom.less'));
+
+ $css = $parser->getCss();
+
+ return $css;
+ }
+}
diff --git a/modules/system/models/MailLayout.php b/modules/system/models/MailLayout.php
index be5fadb75..ef108d8ec 100644
--- a/modules/system/models/MailLayout.php
+++ b/modules/system/models/MailLayout.php
@@ -79,45 +79,55 @@ class MailLayout extends Model
continue;
}
- self::createLayoutFromFile($code, $path);
+ $layout = new static;
+ $layout->code = $code;
+ $layout->is_locked = true;
+ $layout->fillFromView($path);
+ $layout->save();
}
}
- /**
- * Creates a layout using the contents of a specified file.
- * @param string $code New Layout code
- * @param string $viewPath View path
- * @return void
- */
- public static function createLayoutFromFile($code, $viewPath)
+ public function fillFromCode($code = null)
{
- $sections = self::getTemplateSections($viewPath);
+ $definitions = MailManager::instance()->listRegisteredLayouts();
- $name = array_get($sections, 'settings.name', '???');
-
- $css = 'a, a:hover {
- text-decoration: none;
- color: #0862A2;
- font-weight: bold;
+ if ($code === null) {
+ $code = $this->code;
}
- td, tr, th, table {
- padding: 0px;
- margin: 0px;
+ if (!$definition = array_get($definitions, $code)) {
+ throw new ApplicationException('Unable to find a registered layout with code: '.$code);
}
- p {
- margin: 10px 0;
- }';
+ $this->fillFromView($definition);
+ }
- self::create([
- 'is_locked' => true,
- 'name' => $name,
- 'code' => $code,
- 'content_css' => $css,
- 'content_html' => array_get($sections, 'html'),
- 'content_text' => array_get($sections, 'text')
- ]);
+ public function fillFromView($path)
+ {
+ $sections = self::getTemplateSections($path);
+
+ $css = '
+ @media only screen and (max-width: 600px) {
+ .inner-body {
+ width: 100% !important;
+ }
+
+ .footer {
+ width: 100% !important;
+ }
+ }
+
+ @media only screen and (max-width: 500px) {
+ .button {
+ width: 100% !important;
+ }
+ }
+ ';
+
+ $this->name = array_get($sections, 'settings.name', '???');
+ $this->content_css = $css;
+ $this->content_html = array_get($sections, 'html');
+ $this->content_text = array_get($sections, 'text');
}
protected static function getTemplateSections($code)
diff --git a/modules/system/models/MailPartial.php b/modules/system/models/MailPartial.php
index 4f0655262..09c38aeb6 100644
--- a/modules/system/models/MailPartial.php
+++ b/modules/system/models/MailPartial.php
@@ -43,6 +43,13 @@ class MailPartial extends Model
'content_html' => 'required',
];
+ public function afterFetch()
+ {
+ if (!$this->is_custom) {
+ $this->fillFromCode();
+ }
+ }
+
/**
* Loops over each mail layout and ensures the system has a layout,
* if the layout does not exist, it will create one.
@@ -58,29 +65,36 @@ class MailPartial extends Model
continue;
}
- self::createPartialFromFile($code, $path);
+ $partial = new static;
+ $partial->code = $code;
+ $partial->is_custom = 0;
+ $partial->fillFromView($path);
+ $partial->save();
}
}
- /**
- * Creates a layout using the contents of a specified file.
- * @param string $code New Partial code
- * @param string $viewPath View path
- * @return void
- */
- public static function createPartialFromFile($code, $viewPath)
+ public function fillFromCode($code = null)
{
- $sections = self::getTemplateSections($viewPath);
+ $definitions = MailManager::instance()->listRegisteredPartials();
- $name = array_get($sections, 'settings.name', '???');
+ if ($code === null) {
+ $code = $this->code;
+ }
- self::create([
- 'name' => $name,
- 'code' => $code,
- 'is_custom' => 0,
- 'content_html' => array_get($sections, 'html'),
- 'content_text' => array_get($sections, 'text')
- ]);
+ if (!$definition = array_get($definitions, $code)) {
+ throw new ApplicationException('Unable to find a registered partial with code: '.$code);
+ }
+
+ $this->fillFromView($definition);
+ }
+
+ public function fillFromView($path)
+ {
+ $sections = self::getTemplateSections($path);
+
+ $this->name = array_get($sections, 'settings.name', '???');
+ $this->content_html = array_get($sections, 'html');
+ $this->content_text = array_get($sections, 'text');
}
protected static function getTemplateSections($code)
diff --git a/modules/system/models/MailTemplate.php b/modules/system/models/MailTemplate.php
index 6f4c96222..bfb55bd6a 100644
--- a/modules/system/models/MailTemplate.php
+++ b/modules/system/models/MailTemplate.php
@@ -112,6 +112,7 @@ class MailTemplate extends Model
public function fillFromView()
{
$sections = self::getTemplateSections($this->code);
+
$this->content_html = $sections['html'];
$this->content_text = $sections['text'];
$this->subject = array_get($sections, 'settings.subject', 'No subject');
diff --git a/modules/system/models/mailbrandsetting/custom.less b/modules/system/models/mailbrandsetting/custom.less
new file mode 100644
index 000000000..f491d1717
--- /dev/null
+++ b/modules/system/models/mailbrandsetting/custom.less
@@ -0,0 +1,285 @@
+/* Base */
+
+body, body *:not(html):not(style):not(br):not(tr):not(code) {
+ font-family: Avenir, Helvetica, sans-serif;
+ box-sizing: border-box;
+}
+
+body {
+ background-color: @body-bg;
+ color: #74787E;
+ height: 100%;
+ hyphens: auto;
+ line-height: 1.4;
+ margin: 0;
+ -moz-hyphens: auto;
+ -ms-word-break: break-all;
+ width: 100% !important;
+ -webkit-hyphens: auto;
+ -webkit-text-size-adjust: none;
+ word-break: break-all;
+ word-break: break-word;
+}
+
+p,
+ul,
+ol,
+blockquote {
+ line-height: 1.4;
+ text-align: left;
+}
+
+a {
+ color: @link-color;
+}
+
+a img {
+ border: none;
+}
+
+/* Typography */
+
+h1 {
+ color: #2F3133;
+ font-size: 19px;
+ font-weight: bold;
+ margin-top: 0;
+ text-align: left;
+}
+
+h2 {
+ color: #2F3133;
+ font-size: 16px;
+ font-weight: bold;
+ margin-top: 0;
+ text-align: left;
+}
+
+h3 {
+ color: #2F3133;
+ font-size: 14px;
+ font-weight: bold;
+ margin-top: 0;
+ text-align: left;
+}
+
+p {
+ color: #74787E;
+ font-size: 16px;
+ line-height: 1.5em;
+ margin-top: 0;
+ text-align: left;
+}
+
+p.sub {
+ font-size: 12px;
+}
+
+img {
+ max-width: 100%;
+}
+
+/* Layout */
+
+.wrapper {
+ background-color: @body-bg;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.content {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+/* Header */
+
+.header {
+ padding: 25px 0;
+ text-align: center;
+}
+
+.header a {
+ color: #bbbfc3;
+ font-size: 19px;
+ font-weight: bold;
+ text-decoration: none;
+ text-shadow: 0 1px 0 white;
+}
+
+/* Body */
+
+.body {
+ background-color: @content-bg;
+ border-bottom: 1px solid @body-border-color;
+ border-top: 1px solid @body-border-color;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.inner-body {
+ background-color: @content-inner-bg;
+ margin: 0 auto;
+ padding: 0;
+ width: 570px;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 570px;
+}
+
+/* Subcopy */
+
+.subcopy {
+ border-top: 1px solid @subcopy-border-color;
+ margin-top: 25px;
+ padding-top: 25px;
+}
+
+.subcopy p {
+ font-size: 12px;
+}
+
+/* Footer */
+
+.footer {
+ margin: 0 auto;
+ padding: 0;
+ text-align: center;
+ width: 570px;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 570px;
+}
+
+.footer p {
+ color: #AEAEAE;
+ font-size: 12px;
+ text-align: center;
+}
+
+/* Tables */
+
+.table table {
+ margin: 30px auto;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.table th {
+ border-bottom: 1px solid #EDEFF2;
+ padding-bottom: 8px;
+}
+
+.table td {
+ color: #74787E;
+ font-size: 15px;
+ line-height: 18px;
+ padding: 10px 0;
+}
+
+.content-cell {
+ padding: 35px;
+}
+
+/* Buttons */
+
+.action {
+ margin: 30px auto;
+ padding: 0;
+ text-align: center;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.button {
+ border-radius: 3px;
+ box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
+ color: @button-text-color;
+ display: inline-block;
+ text-decoration: none;
+ -webkit-text-size-adjust: none;
+}
+
+.button-primary {
+ background-color: @button-primary-bg;
+ border-top: 10px solid @button-primary-bg;
+ border-right: 18px solid @button-primary-bg;
+ border-bottom: 10px solid @button-primary-bg;
+ border-left: 18px solid @button-primary-bg;
+}
+
+.button-positive {
+ background-color: @button-positive-bg;
+ border-top: 10px solid @button-positive-bg;
+ border-right: 18px solid @button-positive-bg;
+ border-bottom: 10px solid @button-positive-bg;
+ border-left: 18px solid @button-positive-bg;
+}
+
+.button-negative {
+ background-color: @button-negative-bg;
+ border-top: 10px solid @button-negative-bg;
+ border-right: 18px solid @button-negative-bg;
+ border-bottom: 10px solid @button-negative-bg;
+ border-left: 18px solid @button-negative-bg;
+}
+
+/* Panels */
+
+.panel {
+ margin: 0 0 21px;
+}
+
+.panel-content {
+ background-color: #EDEFF2;
+ padding: 16px;
+}
+
+.panel-item {
+ padding: 0;
+}
+
+.panel-item p:last-of-type {
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+
+/* Promotions */
+
+.promotion {
+ background-color: #FFFFFF;
+ border: 2px dashed #9BA2AB;
+ margin: 0;
+ margin-bottom: 25px;
+ margin-top: 25px;
+ padding: 24px;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.promotion h1 {
+ text-align: center;
+}
+
+.promotion p {
+ font-size: 15px;
+ text-align: center;
+}
diff --git a/modules/system/models/mailbrandsetting/fields.yaml b/modules/system/models/mailbrandsetting/fields.yaml
new file mode 100644
index 000000000..a58ca61cb
--- /dev/null
+++ b/modules/system/models/mailbrandsetting/fields.yaml
@@ -0,0 +1,122 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+
+ _mail_preview:
+ type: partial
+ path: field_mail_preview
+
+secondaryTabs:
+ fields:
+
+ _section_background:
+ label: Background
+ type: section
+
+ body_bg:
+ label: Body background
+ type: colorpicker
+ availableColors: []
+
+ content_bg:
+ label: Content background
+ type: colorpicker
+ availableColors: []
+
+ content_inner_bg:
+ label: Inner content background
+ type: colorpicker
+ availableColors: []
+
+ _section_buttons:
+ label: Buttons
+ type: section
+
+ button_text_color:
+ label: Button text color
+ type: colorpicker
+ availableColors: []
+
+ button_primary_bg:
+ label: Primary button background
+ type: colorpicker
+ availableColors: []
+
+ button_positive_bg:
+ label: Positive button background
+ type: colorpicker
+ availableColors: []
+
+ button_negative_bg:
+ label: Negative button background
+ type: colorpicker
+ availableColors: []
+
+ _section_type:
+ label: Typography
+ type: section
+
+ header_color:
+ label: Site name color
+ type: colorpicker
+ availableColors: []
+
+ heading_color:
+ label: Heading color
+ type: colorpicker
+ availableColors: []
+
+ text_color:
+ label: Text color
+ type: colorpicker
+ availableColors: []
+
+ link_color:
+ label: Link color
+ type: colorpicker
+ availableColors: []
+
+ footer_color:
+ label: Footer color
+ type: colorpicker
+ availableColors: []
+
+ _section_borders:
+ label: Borders
+ type: section
+
+ body_border_color:
+ label: Body border color
+ type: colorpicker
+ availableColors: []
+
+ subcopy_border_color:
+ label: Subcopy border color
+ type: colorpicker
+ availableColors: []
+
+ table_border_color:
+ label: Table border color
+ type: colorpicker
+ availableColors: []
+
+ _section_components:
+ label: Components
+ type: section
+
+ panel_bg:
+ label: Panel background
+ type: colorpicker
+ availableColors: []
+
+ promotion_bg:
+ label: Promotion background
+ type: colorpicker
+ availableColors: []
+
+ promotion_border_color:
+ label: Promotion border color
+ type: colorpicker
+ availableColors: []
diff --git a/modules/system/models/mailbrandsetting/sample_template.htm b/modules/system/models/mailbrandsetting/sample_template.htm
new file mode 100644
index 000000000..66fc96dc2
--- /dev/null
+++ b/modules/system/models/mailbrandsetting/sample_template.htm
@@ -0,0 +1,55 @@
+{% component 'header' %}
+October CMS
+{% endcomponent %}
+
+# Heading 1
+
+This is a paragraph filled with Lorem Ipsum and a link.
+Cumque dicta doloremque eaque, enim error laboriosam pariatur possimus tenetur veritatis voluptas.
+
+## Heading 2
+
+{% component 'table' %}
+| Item | Description | Price |
+| ------------- |:-------------:| --------:|
+| Item 1 | Centered | $10 |
+| Item 2 | Right-Aligned | $20 |
+{% endcomponent %}
+
+### Heading 3
+
+This is a paragraph filled with Lorem Ipsum and a link.
+Cumque dicta doloremque eaque, enim error laboriosam pariatur possimus tenetur veritatis voluptas.
+
+{% component 'button' url='javascript:;' %}
+Primary button
+{% endcomponent %}
+
+{% component 'button' type='positive' url='javascript:;' %}
+Positive button
+{% endcomponent %}
+
+{% component 'button' type='negative' url='javascript:;' %}
+Negative button
+{% endcomponent %}
+
+{% component 'panel' %}
+How awesome is this panel?
+{% endcomponent %}
+
+Some more text
+
+{% component 'promotion' %}
+Coupon code: OCTOBER
+{% endcomponent %}
+
+Thanks,
+October CMS
+
+{% component 'subcopy' %}
+This is the subcopy of the email
+{% endcomponent %}
+
+{% component 'footer' %}
+© 2017 October CMS. All rights reserved.
+{% endcomponent %}