From 2c062b9d907327c947433d16b2bd115dee547ab0 Mon Sep 17 00:00:00 2001 From: Marten Date: Thu, 30 Jul 2020 17:22:14 +0200 Subject: [PATCH] feat: added br\n filter + suffix for copyright and fixed table issues --- .../io/rocketbase/mail/CopyrightLine.java | 6 + .../rocketbase/mail/EmailTemplateBuilder.java | 30 +++++ .../resources/templates/email/layout.html | 26 ++--- .../main/resources/templates/email/layout.txt | 2 +- .../mail/EmailTemplateBuilderTest.java | 106 ++++++++++++------ template/src/layout.html | 26 +++-- 6 files changed, 135 insertions(+), 61 deletions(-) diff --git a/email-template-builder/src/main/java/io/rocketbase/mail/CopyrightLine.java b/email-template-builder/src/main/java/io/rocketbase/mail/CopyrightLine.java index 2e5e053..c24a48f 100644 --- a/email-template-builder/src/main/java/io/rocketbase/mail/CopyrightLine.java +++ b/email-template-builder/src/main/java/io/rocketbase/mail/CopyrightLine.java @@ -12,6 +12,7 @@ public class CopyrightLine implements TemplateLine { final String name; Integer year; String url; + String suffix; @Getter(AccessLevel.PRIVATE) EmailTemplateBuilder.EmailTemplateConfigBuilder builder; @@ -32,6 +33,11 @@ public CopyrightLine url(String url) { return this; } + public CopyrightLine suffix(String suffix) { + this.suffix = suffix; + return this; + } + @Override public TemplateLineType getType() { return TemplateLineType.COPYRIGHT; diff --git a/email-template-builder/src/main/java/io/rocketbase/mail/EmailTemplateBuilder.java b/email-template-builder/src/main/java/io/rocketbase/mail/EmailTemplateBuilder.java index 8de40a9..1f9cfa2 100644 --- a/email-template-builder/src/main/java/io/rocketbase/mail/EmailTemplateBuilder.java +++ b/email-template-builder/src/main/java/io/rocketbase/mail/EmailTemplateBuilder.java @@ -2,6 +2,9 @@ import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.error.PebbleException; +import com.mitchellbosecke.pebble.extension.AbstractExtension; +import com.mitchellbosecke.pebble.extension.Filter; +import com.mitchellbosecke.pebble.template.EvaluationContext; import com.mitchellbosecke.pebble.template.PebbleTemplate; import io.rocketbase.mail.config.TbConfiguration; import io.rocketbase.mail.model.HtmlTextEmail; @@ -23,6 +26,7 @@ public final class EmailTemplateBuilder { private static final PebbleEngine ENGINE = new PebbleEngine.Builder() .strictVariables(false) .autoEscaping(false) + .extension(new PebbleEmailExtension()) .build(); public static EmailTemplateConfigBuilder builder() { @@ -30,6 +34,32 @@ public static EmailTemplateConfigBuilder builder() { } + public static class PebbleEmailExtension extends AbstractExtension { + @Override + public Map getFilters() { + Map filters = new HashMap<>(); + filters.put("br", new BrFilter()); + return filters; + } + + public static final class BrFilter implements Filter { + + @Override + public Object apply(Object o, Map map, PebbleTemplate pebbleTemplate, EvaluationContext evaluationContext, int i) throws PebbleException { + if (o == null) { + return null; + } + return o.toString().replace("\n", "
"); + } + + @Override + public List getArgumentNames() { + return null; + } + } + } + + @SneakyThrows static HtmlTextEmail build(TbConfiguration configuration, Header header, List contentLines, List footerLines) throws PebbleException { PebbleTemplate htmlTemplate = ENGINE.getTemplate("templates/email/layout.html"); diff --git a/email-template-builder/src/main/resources/templates/email/layout.html b/email-template-builder/src/main/resources/templates/email/layout.html index c19875c..4ffa22a 100644 --- a/email-template-builder/src/main/resources/templates/email/layout.html +++ b/email-template-builder/src/main/resources/templates/email/layout.html @@ -162,11 +162,6 @@ font-size: {{ c.table.heading.size | default('12px') }}; } - .table_footer { - padding-top: 15px; - border-top: {{ c.table.border.size | default('1px') }} solid {{ c.table.border.color | default('#EAEAEC') }}; - } - body { background-color: {{ c.body.background | default('#F4F4F7') }}; color: {{ c.text.color | default('#51545E') }}; @@ -338,7 +333,7 @@
{% elseif line.type == 'TEXT' %} -

{% if line.linkUrl is not empty %}{% endif %}{{ line.text | escape }}{% if line.linkUrl is not empty %}{% endif %}

+

{% if line.linkUrl is not empty %}{% endif %}{{ line.text | escape | br | raw }}{% if line.linkUrl is not empty %}{% endif %}

{% elseif line.type == 'HTML' %}

{{ line.html | raw }}

@@ -399,7 +394,7 @@ {% for col in head %} -

{{ col | escape }}

+

{{ col | escape | br | raw }}

{% endfor %} @@ -409,15 +404,15 @@ {% for col in item %} {% if col.type is empty %} - {% if line.item[loop.index].numberFormat is empty %}{{ col | escape }}{% else %}{{ col | numberformat(line.item[loop.index].numberFormat) }}{% endif %} + {% if line.item[loop.index].numberFormat is empty %}{{ col | escape | br | raw }}{% else %}{{ col | numberformat(line.item[loop.index].numberFormat) }}{% endif %} {% elseif col.type == 'LINK' %} {{ col.text | escape }} {% elseif col.type == 'IMAGE' %} - {% if col.image.linkUrl is not empty %}{% endif %} - {{- col.image.alt | default('') | escape -}} - {% if col.image.linkUrl is not empty %}{% endif %} + {% if col.linkUrl is not empty %}{% endif %} + {{- col.alt | default('') | escape -}} + {% if col.linkUrl is not empty %}{% endif %} {% endif %} @@ -426,9 +421,10 @@ {% endfor %} {% for footer in line.footerRows %} + {% set firstLoop = loop.first %}{% set singleLoop = (loop.length <= 1) %} {% for col in footer %} - -

{% if line.item[loop.index].numberFormat is empty %}{{ col | escape }}{% else %}{{ col | numberformat(line.item[loop.index].numberFormat) }}{% endif %}

+ +

{% if line.item[loop.index].numberFormat is empty %}{{ col | escape | br | raw }}{% else %}{{ col | numberformat(line.item[loop.index].numberFormat) }}{% endif %}

{% endfor %} @@ -458,10 +454,10 @@
{% elseif line.type == 'COPYRIGHT' %} -

©{{ line.year }} {% if line.url is not empty %}{% endif %}{{ line.name | escape }}{% if line.url is not empty %}{% endif %}

+

©{{ line.year }} {% if line.url is not empty %}{% endif %}{{ line.name | escape }}{% if line.url is not empty %}{% endif %}{% if line.suffix is not empty %}{{ line.suffix }}{% endif %}

{% elseif line.type == 'TEXT' %} -

{% if line.linkUrl is not empty %}{% endif %}{{ line.text | escape }}{% if line.linkUrl is not empty %}{% endif %}

+

{% if line.linkUrl is not empty %}{% endif %}{{ line.text | br | raw }}{% if line.linkUrl is not empty %}{% endif %}

{% elseif line.type == 'HTML' %}

{{ line.html | raw }}

diff --git a/email-template-builder/src/main/resources/templates/email/layout.txt b/email-template-builder/src/main/resources/templates/email/layout.txt index 460b6c4..d179bda 100644 --- a/email-template-builder/src/main/resources/templates/email/layout.txt +++ b/email-template-builder/src/main/resources/templates/email/layout.txt @@ -57,7 +57,7 @@ TABLE IS NOT YET AVAILABLE ON TXT-EMAIL {% elseif line.type == 'COPYRIGHT' %} -©{{ line.year }} {{line.name }}{% if line.url is not empty %} -> {{ line.url | raw }}{% endif %} +©{{ line.year }} {{line.name }}{% if line.url is not empty %} -> {{ line.url | raw }}{% endif %}{% if line.suffix is not empty %}{{ line.suffix }}{% endif %} {% elseif line.type == 'TEXT' %} diff --git a/email-template-builder/src/test/java/io/rocketbase/mail/EmailTemplateBuilderTest.java b/email-template-builder/src/test/java/io/rocketbase/mail/EmailTemplateBuilderTest.java index d1b78bc..59fc06a 100644 --- a/email-template-builder/src/test/java/io/rocketbase/mail/EmailTemplateBuilderTest.java +++ b/email-template-builder/src/test/java/io/rocketbase/mail/EmailTemplateBuilderTest.java @@ -2,8 +2,6 @@ import io.rocketbase.mail.config.TbConfiguration; import io.rocketbase.mail.model.HtmlTextEmail; -import io.rocketbase.mail.styling.ColorStyle; -import io.rocketbase.mail.styling.ColorStyleSimple; import org.junit.Test; import org.simplejavamail.api.email.Email; import org.simplejavamail.api.mailer.Mailer; @@ -22,6 +20,7 @@ public class EmailTemplateBuilderTest { protected Mailer getMailer() { if (mailer == null) { + // default mailer = MailerBuilder.withSMTPServer("localhost", 1025) .buildMailer(); } @@ -31,14 +30,15 @@ protected Mailer getMailer() { protected void sentEmail(String subject, HtmlTextEmail content) { try { Email email = EmailBuilder.startingBlank() - .to("hello@test.de") - .from("test@localhost") + .to("melistik@icloud.com") + .from("service@rocketbase.io") .withSubject(subject) .withHTMLText(content.getHtml()) .withPlainText(content.getText()) .buildEmail(); getMailer().sendMail(email); } catch (Exception e) { + // ignore error here - works only on local machine for visible test-purpose } } @@ -49,35 +49,28 @@ public void standardTestHtml() { EmailTemplateBuilder.EmailTemplateConfigBuilder builder = EmailTemplateBuilder.builder(); String header = "test"; // when - TbConfiguration config = TbConfiguration.newInstance(); - config.getContent().setWidth(800); - config.getContent().setFull(true); HtmlTextEmail htmlTextEmail = builder - .configuration(config) .header() .logo("https://www.rocketbase.io/img/logo-dark.png").logoHeight(41) .and() - .text("sample-text").and() - .text("link to google").linkUrl("https://www.google").and() - .text("link to rocketbase").bold().underline().linkUrl("https://www.rocketbase.io").color(ColorStyle.RED).center().and() - .image("https://cdn.rocketbase.io/assets/loading/no-image.jpg").alt("no-picture").width(300).center().and() - .hr().margin("20px 0").and() - .image("https://cdn.rocketbase.io/assets/signature/rocketbase-logo-signature-2020.png").alt("rocketbase").width(150).linkUrl("https://www.rocketbase.io").right().and() - .button("click me here", "http://localhost").red().right().and() - .button("gray is the new pink", "http://localhost").gray().left().and() - .button("button 1", "http://adasd").and() - .text("sample text").and() + .text("Welcome, {{name}}!").h1().center().and() + .text("Thanks for trying [Product Name]. We’re thrilled to have you on board. To get the most out of [Product Name], do this primary next step:").and() + .button("Do this Next", "http://localhost").blue().and() + .text("For reference, here's your login information:").and() .attribute() - .keyValue("KEY 1", "Value 123") - .keyValue("KEY 2", "Value ABC") + .keyValue("Login Page", "{{login_url}}") + .keyValue("Username", "{{username}}") .and() - .text("another text").and() - .copyright("rocketbase").url("https://www.rocketbase.io").and() - .footerText("my agb can be found here").linkUrl("http://localhost").and() - .footerImage("https://cdn.rocketbase.io/assets/loading/no-image.jpg").height(50).right().and() - .footerHr().and() - .footerText("my little text").underline().bold().and() - .footerImage("https://cdn.rocketbase.io/assets/loading/no-image.jpg").width(100).left().linkUrl("https://www.rocketbase.io").and() + .html("If you have any questions, feel free to email our customer success team. (We're lightning quick at replying.) We also offer live chat during business hours.", + "If you have any questions, feel free to email our customer success team\n" + + "(We're lightning quick at replying.) We also offer live chat during business hours.").and() + .text("Cheers,\n" + + "The [Product Name] Team").and() + .copyright("rocketbase").url("https://www.rocketbase.io").suffix(". All rights reserved.").and() + .footerText("[Company Name, LLC]\n" + + "1234 Street Rd.\n" + + "Suite 1234").and() + .footerImage("https://cdn.rocketbase.io/assets/loading/no-image.jpg").width(100).linkUrl("https://www.rocketbase.io").and() .build(); // then assertThat(htmlTextEmail, notNullValue()); @@ -90,26 +83,73 @@ public void standardTableTestHtml() { // given EmailTemplateBuilder.EmailTemplateConfigBuilder builder = EmailTemplateBuilder.builder(); - String header = "test"; - ColorStyleSimple headerStyle = new ColorStyleSimple("000000", "ff0000"); + String header = "Invoice {{invoice_id}}"; // when + TbConfiguration config = TbConfiguration.newInstance(); + config.getContent().setFull(true); HtmlTextEmail htmlTextEmail = builder + .configuration(config) .header().text(header).and() - .text("sample-text").and() + .text("Hi {{name}},").and() + .text("Thanks for using [Product Name]. This is an invoice for your recent purchase").and() .tableSimple("#.## '€'") .headerRow("Description", "Amount") - .itemRow("Special Product", BigDecimal.valueOf(1333, 2)) + .itemRow("Special Product\n" + + "Some extra explanations in separate line", BigDecimal.valueOf(1333, 2)) .itemRow("Short service", BigDecimal.valueOf(103, 1)) .footerRow("Total", BigDecimal.valueOf(2363, 2)) .and() - .button("button 1", "http://adasd").and() - .copyright("rocketbase").url("https://www.rocketbase.io") + .button("Download PDF", "http://localhost").gray().right().and() + .text("If you have any questions about this receipt, simply reply to this email or reach out to our support team for help.").and() + .copyright("rocketbase").url("https://www.rocketbase.io").suffix(". All rights reserved.").and() + .footerText("[Company Name, LLC]\n" + + "1234 Street Rd.\n" + + "Suite 1234").and() .build(); // then assertThat(htmlTextEmail, notNullValue()); sentEmail("standardTableTestHtml", htmlTextEmail); } + @Test + public void standardTableExtendedTestHtml() { + // given + EmailTemplateBuilder.EmailTemplateConfigBuilder builder = EmailTemplateBuilder.builder(); + + String header = "Invoice {{invoice_id}}"; + // when + TbConfiguration config = TbConfiguration.newInstance(); + config.getContent().setWidth(800); + + HtmlTextEmail htmlTextEmail = builder + .configuration(config) + .header().text(header).and() + .text("Hi {{name}},").and() + .text("Thanks for using [Product Name]. This is an invoice for your recent purchase").and() + .tableSimpleWithImage("#.## '€'") + .headerRow("Preview", "Description", "Amount") + .itemRow("https://cdn.shopify.com/s/files/1/0255/1211/6260/products/TCW1142-07052_small.jpg?v=1589200198", "Damen Harbour Tanktop × 1\n" + + "QUARTZ PINK / S", BigDecimal.valueOf(4995, 2)) + .itemRow("https://cdn.shopify.com/s/files/1/0255/1211/6260/products/TCM1886-0718_201_fdf0be52-639f-4ea8-9143-6bd75e0821b1_small.jpg?v=1583509609", "Herren ten Classic T-Shirt\n"+ + "FOREST GREEN HEATHER / XL", BigDecimal.valueOf(3995, 2)) + .itemRow("https://cdn.shopify.com/s/files/1/0255/1211/6260/products/TCM1939-0439_1332_da6f3e7c-e18d-4778-be97-c6c0b482b643_small.jpg?v=1583509671", "Herren Joshua Hanfshorts\n" + + "DARK OCEAN BLUE / XL", BigDecimal.valueOf(6995, 2)) + .footerRow("Sum", BigDecimal.valueOf(15985, 2)) + .footerRow("Code - PLANT5", BigDecimal.valueOf(-799, 2)) + .footerRow("Total incl. Tax\n", BigDecimal.valueOf(15186, 2)) + .and() + .button("Download PDF", "http://localhost").gray().right().and() + .text("If you have any questions about this receipt, simply reply to this email or reach out to our support team for help.").and() + .copyright("rocketbase").url("https://www.rocketbase.io").suffix(". All rights reserved.").and() + .footerText("[Company Name, LLC]\n" + + "1234 Street Rd.\n" + + "Suite 1234").and() + .build(); + // then + assertThat(htmlTextEmail, notNullValue()); + sentEmail("standardTableExtendedTestHtml", htmlTextEmail); + } + @Test public void withoutHeaderAndFooterHtml() { // given diff --git a/template/src/layout.html b/template/src/layout.html index 31c2745..33231cd 100644 --- a/template/src/layout.html +++ b/template/src/layout.html @@ -91,6 +91,12 @@ margin: {{ c.text.margin | default('.4em 0 1.1875em') }}; line-height: {{ c.text.lineHeight | default('1.625') }}; } + + .table_footer p { + margin: auto; + line-height: auto; + } + /* Utilities ------------------------------ */ .align-right { @@ -162,11 +168,6 @@ font-size: {{ c.table.heading.size | default('12px') }}; } - .table_footer { - padding-top: 15px; - border-top: {{ c.table.border.size | default('1px') }} solid {{ c.table.border.color | default('#EAEAEC') }}; - } - body { background-color: {{ c.body.background | default('#F4F4F7') }}; color: {{ c.text.color | default('#51545E') }}; @@ -340,7 +341,7 @@
{% elseif line.type == 'TEXT' %} -

{% if line.linkUrl is not empty %}{% endif %}{{ line.text | escape }}{% if line.linkUrl is not empty %}{% endif %}

+

{% if line.linkUrl is not empty %}{% endif %}{{ line.text | escape | br | raw }}{% if line.linkUrl is not empty %}{% endif %}

{% elseif line.type == 'HTML' %}

{{ line.html | raw }}

@@ -403,7 +404,7 @@ {% for col in head %} -

{{ col | escape }}

+

{{ col | escape | br | raw }}

{% endfor %} @@ -413,7 +414,7 @@ {% for col in item %} {% if col.type is empty %} - {% if line.item[loop.index].numberFormat is empty %}{{ col | escape }}{% else %}{{ col | numberformat(line.item[loop.index].numberFormat) }}{% endif %} + {% if line.item[loop.index].numberFormat is empty %}{{ col | escape | br | raw }}{% else %}{{ col | numberformat(line.item[loop.index].numberFormat) }}{% endif %} {% elseif col.type == 'LINK' %} {{ col.text | escape }} @@ -430,9 +431,10 @@ {% endfor %} {% for footer in line.footerRows %} + {% set firstLoop = loop.first %} {% for col in footer %} - -

{% if line.item[loop.index].numberFormat is empty %}{{ col | escape }}{% else %}{{ col | numberformat(line.item[loop.index].numberFormat) }}{% endif %}

+ +

{% if line.item[loop.index].numberFormat is empty %}{{ col | escape | br | raw }}{% else %}{{ col | numberformat(line.item[loop.index].numberFormat) }}{% endif %}

{% endfor %} @@ -462,10 +464,10 @@
{% elseif line.type == 'COPYRIGHT' %} -

©{{ line.year }} {% if line.url is not empty %}{% endif %}{{ line.name | escape }}{% if line.url is not empty %}{% endif %}

+

©{{ line.year }} {% if line.url is not empty %}{% endif %}{{ line.name | escape }}{% if line.url is not empty %}{% endif %}{% if line.suffix is not empty %}{{ line.suffix }}{% endif %}

{% elseif line.type == 'TEXT' %} -

{% if line.linkUrl is not empty %}{% endif %}{{ line.text | escape }}{% if line.linkUrl is not empty %}{% endif %}

+

{% if line.linkUrl is not empty %}{% endif %}{{ line.text | escape | br | raw }}{% if line.linkUrl is not empty %}{% endif %}

{% elseif line.type == 'HTML' %}

{{ line.html | raw }}