Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions core-customize/hybris/bin/custom/cxdevtools/cxdevtoolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,49 @@ the configuration file `systemsetup-spring.xml` and inspect the project data imp

## Centralized HtmlEmail handling

The main purpose of these services is, to provide a simple way of sending emails to the customers, without making use of the `CMSComponents`
like with the standard way of mailing within the SAP Commerce Cloud, provided by the `acceleratorservices` extension. The implementation
makes use of the [Thymeleaf rendering engine](https://www.thymeleaf.org/), i.e. you are able to define your mails as thymeleaf templates
and provide localized messages to it.

The `HtmlEmailGenerator` services (registered as bean with name `thymeleafHtmlEmailGenerator` and alias `htmlEmailGenerator`) should be used
to create `HtmlEmail` objects, whenever you want to send an email, e.g. from Workflows or from EventListeners. The class provides a simple
and an enhanced mechanism to create `HtmlEmail` objects. The simple way takes a `String` as a body and sets it as HTML body for the email.
The enhanced mechanism takes a template name and context parameters. The template is resolves from the classpath and the template engine is
executed with the provided context parameters.
The main purpose of these services is to provide a simple way of sending emails without relying on CMSComponents, as
required by the standard SAP Commerce Cloud acceleratorservices approach. The implementation uses the Thymeleaf
rendering engine.

### Template resolution

Email templates are stored as localized `ThymeleafEmailTemplate` items in the database and can be managed via
Backoffice (under *CX DEV Tools → Email*) or Impex.

At runtime, `EmailTemplateResolverService` (impl: `DatabaseEmailTemplateResolverService`) resolves the matching
`ThymeleafEmailTemplate` by `code` and locale, and passes the localized HTML content to `HtmlEmailBuilder` for
Thymeleaf rendering.

### Impex import

Templates can be created or updated via Impex:

```impex
INSERT_UPDATE ThymeleafEmailTemplate; code[unique = true]; template[lang = de] ; template[lang = en]
; contact ; "<!DOCTYPE html>
<html>
<body>
<p th:text=""'Name: ' + $d{firstName} + ' ' + $d{lastName}""></p>
<p th:text=""'E-Mail: ' + $d{email}""></p>
</body>
</html>" ;
```

**Impex workaround for Thymeleaf expressions:** Impex interprets `${...}` as its own variable syntax. To work around
this:

- Define the macro `$d=$` at the top of the Impex file. This causes `$d{...}` to be resolved to `${...}` during
import.
- Use `$d{...}` instead of `${...}` for all Thymeleaf expressions in the file.
- Escape all double quotes inside HTML values by doubling them (`"` → `""`).


### HtmlEmailGenerator (programmatic use)

The `HtmlEmailGenerator` bean (`thymeleafHtmlEmailGenerator` / alias `htmlEmailGenerator`) remains available for
programmatic email creation, e.g. from Workflows or EventListeners. It provides a simple mode (plain HTML string as
body) and an enhanced mode (template code + locale + context parameters).


### How to activate and use

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,30 +87,6 @@ cxdevtoolkit.impeximport.environment.importtestdata=false
# update.executeProjectData.extensionName.list=cxdevtoolkit
##############################################################################


##############################################################################
# Html Email Service configuration
#
# The HtmlEmailService requires all templates to be stored within the following class paths:
# cxdevtoolkit/email-templates/html/**
# cxdevtoolkit/email-templates/text/**
#
# For localization of the emails, message bundles are preconfigured. Unfortunately, a message source cannot
# hold wildcards and, therefore, this extension has 5 prepared configuration paths that are resolved during
# system startup. If you want to contribute to the message bundle for email templates, please take one of the
# properties below, and change the path accordingly. The order is important, see note below!
#
# Note: You cannot simply add your messages to a file called cxdevtoolkit/email-templates/messages, because the message
# source will stop file resolving, after the first match, ie. a file already existing in a previous message
# source will stop the resolver from loading your keys from your local file with the same name.
##############################################################################
cxdevtoolkit.htmlEmailService.configuration.messagebundle1=classpath*:cxdevtoolkit/email-templates/messages
cxdevtoolkit.htmlEmailService.configuration.messagebundle2=classpath*:cxdevtoolkit/email-templates/messages
cxdevtoolkit.htmlEmailService.configuration.messagebundle3=classpath*:cxdevtoolkit/email-templates/messages
cxdevtoolkit.htmlEmailService.configuration.messagebundle4=classpath*:cxdevtoolkit/email-templates/messages
cxdevtoolkit.htmlEmailService.configuration.messagebundle5=classpath*:cxdevtoolkit/email-templates/messages


##############################################################################
# Html Email Service Fake configuration
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<context component="explorer-tree" merge-by="module">
<explorer-tree:explorer-tree>
<explorer-tree:navigation-node id="cxdevtools_treenode_main" position="999">
<explorer-tree:navigation-node id="cxdevtools_treenode_email" position="499">
<explorer-tree:type-node code="ThymeleafEmailTemplate" id="cxdevtools_typenode_ThymeleafEmailTemplate"/>
</explorer-tree:navigation-node>
<explorer-tree:navigation-node id="cxdevtools_treenode_fakemail" position="500">
<explorer-tree:type-node code="LocallyStoredEmail" id="cxdevtools_typenode_storedemail"/>
</explorer-tree:navigation-node>
Expand Down Expand Up @@ -79,4 +82,34 @@
</editorArea:tab>
</editorArea:editorArea>
</context>

<!-- editor-area for ThymeleafEmailTemplate -->
<context merge-by="type" parent="GenericItem" type="ThymeleafEmailTemplate" component="base">
<y:base>
<y:labels>
<y:label>'Email Template' + (code != null ? '' + ': ' + code : '')</y:label>
</y:labels>
</y:base>
</context>

<context merge-by="type" parent="GenericItem" type="ThymeleafEmailTemplate" component="editor-area">
<editorArea:editorArea xmlns:editorArea="http://www.hybris.com/cockpitng/component/editorArea">
<editorArea:tab name="hmc.tab.common">

<editorArea:section name="hmc.properties">
<editorArea:attribute qualifier="code"/>
</editorArea:section>

<editorArea:section name="cxdevtools_section_ThymeleafEmailTemplate_content" columns="1">
<editorArea:attribute qualifier="template" editor="com.hybris.cockpitng.editor.localized(com.hybris.cockpitng.editor.defaulttext)">
<editorArea:editor-parameter>
<editorArea:name>rows</editorArea:name>
<editorArea:value>15</editorArea:value>
</editorArea:editor-parameter>
</editorArea:attribute>
</editorArea:section>

</editorArea:tab>
</editorArea:editorArea>
</context>
</config>
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
cxdevtools_treenode_main=CX DEV Tools
cxdevtools_treenode_fakemail=Fake Mails
cxdevtools_treenode_fakemail=Fake Mails
cxdevtools_treenode_email=E-Mail

cxdevtools_section_ThymeleafEmailTemplate_content=Template Inhalt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
cxdevtools_treenode_main=CX DEV Tools
cxdevtools_treenode_fakemail=Fake Mails
cxdevtools_treenode_fakemail=Fake Mails
cxdevtools_treenode_email=Email

cxdevtools_section_ThymeleafEmailTemplate_content=Template Content
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,39 @@
</attribute>
</attributes>
</itemtype>
<itemtype code="ThymeleafEmailTemplate">
<deployment table="thymeleafemailtemplate" typecode="31141"/>
<attributes>
<attribute qualifier="code" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" unique="true"/>
</attribute>
<attribute qualifier="template" type="localized:java.lang.String">
<persistence type="property">
<columntype>
<value>HYBRIS.LONG_STRING</value>
</columntype>
<columntype database="mysql">
<value>TEXT</value>
</columntype>
<columntype database="oracle">
<value>CLOB</value>
</columntype>
<columntype database="sqlserver">
<value>TEXT</value>
</columntype>
<columntype database="sap">
<value>NCLOB</value>
</columntype>
</persistence>
<modifiers read="true" write="true"/>
</attribute>
</attributes>
<indexes>
<index name="code" unique="true">
<key attribute="code"/>
</index>
</indexes>
</itemtype>
</itemtypes>
</items>
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,15 @@
<import resource="cxdevtoolkit/systemsetup-spring.xml"/>
<import resource="cxdevtoolkit/thymeleafemails-spring.xml"/>
<import resource="cxdevtoolkit/thymeleafemails-fake-spring.xml"/>

<alias name="databaseEmailTemplateResolverService" alias="emailTemplateResolverService"/>
<bean name="databaseEmailTemplateResolverService"
class="me.cxdev.commerce.toolkit.email.service.impl.DatabaseEmailTemplateResolverService">
<constructor-arg name="emailTemplateResolverDao" ref="emailTemplateResolverDao"/>
</bean>

<alias name="defaultEmailTemplateDao" alias="emailTemplateResolverDao"/>
<bean name="defaultEmailTemplateDao" class="me.cxdev.commerce.toolkit.email.dao.impl.DatabaseEmailTemplateResolverDao">
<constructor-arg name="flexibleSearchService" ref="flexibleSearchService"/>
</bean>
</beans>
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,6 @@

<alias name="thymeleafHtmlEmailMessageSource" alias="htmlEmailMessageSource"/>
<bean id="thymeleafHtmlEmailMessageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<array value-type="java.lang.String">
<value>${cxdevtoolkit.htmlEmailService.configuration.messagebundle1}</value>
<value>${cxdevtoolkit.htmlEmailService.configuration.messagebundle2}</value>
<value>${cxdevtoolkit.htmlEmailService.configuration.messagebundle3}</value>
<value>${cxdevtoolkit.htmlEmailService.configuration.messagebundle4}</value>
<value>${cxdevtoolkit.htmlEmailService.configuration.messagebundle5}</value>
</array>
</property>
<property name="useCodeAsDefaultMessage" value="true"/>
<property name="defaultEncoding" value="UTF-8" />
<property name="fallbackToSystemLocale" value="false"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
type.LocallyStoredEmail.name=Lokale Email
type.LocallyStoredEmail.description=Lokal gespeicherte Email durch einen Fake Service, anstatt sie via SMTP zu versenden.
type.LocallyStoredEmail.name=Lokale E-Mail
type.LocallyStoredEmail.description=Lokal gespeicherte E-Mail durch einen Fake Service, anstatt sie via SMTP zu versenden.
type.LocallyStoredEmail.sender.name=Absender
type.LocallyStoredEmail.recipients.name=Empfänger
type.LocallyStoredEmail.recipients.name=Empf\u00E4nger

type.ThymeleafEmailTemplate.name=E-Mail-Template
type.ThymeleafEmailTemplate.description=Speichert lokalisierte HTML-Vorlagen, die von der Thymeleaf-Engine f\u00FCr E-Mails verwendet werden.
type.ThymeleafEmailTemplate.code.name=Template-Name
type.ThymeleafEmailTemplate.code.description=Eindeutige Kennung zur Aufl\u00F6sung der Vorlage (z. B. 'contact', 'registration')
type.ThymeleafEmailTemplate.template.name=HTML-Vorlage
type.ThymeleafEmailTemplate.template.description=Roher HTML-Code, der das Thymeleaf-Markup f\u00FCr den E-Mail-Body enth\u00E4lt
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
type.LocallyStoredEmail.name=Local Email
type.LocallyStoredEmail.description=Email stored locally by a fake service instead of sending them via SMTP.
type.LocallyStoredEmail.sender.name=Sender
type.LocallyStoredEmail.recipients.name=Recipients
type.LocallyStoredEmail.recipients.name=Recipients

type.ThymeleafEmailTemplate.name=Email Template
type.ThymeleafEmailTemplate.description=Stores localized HTML templates processed by the Thymeleaf engine for emails.
type.ThymeleafEmailTemplate.code.name=Template Name
type.ThymeleafEmailTemplate.code.description=Unique identifier used to resolve the template (e.g., 'contact', 'registration')
type.ThymeleafEmailTemplate.template.name=HTML Template
type.ThymeleafEmailTemplate.template.description=Raw HTML code containing the Thymeleaf markup for the email body
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ static HtmlEmailBuilder withHtmlEmailGenerator(HtmlEmailGenerator generator) {

private String subject;
private String body;
private String templateName;
private String templateHtml;
private Map<String, Object> contextParameters;
private Locale templateLocale;

Expand Down Expand Up @@ -114,9 +114,8 @@ public HtmlEmailBuilder body(String body) {
return this;
}

public HtmlEmailBuilder template(String template, Locale locale) {
this.templateName = template;
this.templateLocale = locale;
public HtmlEmailBuilder template(String template) {
this.templateHtml = template;
return templateParameter(Map.of());
}

Expand All @@ -133,6 +132,11 @@ public HtmlEmailBuilder templateParameter(Map<String, Object> parameters) {
return this;
}

public HtmlEmailBuilder locale(Locale locale) {
this.templateLocale = locale;
return this;
}

public HtmlEmailBuilder attach(File file) {
attach(HtmlEmailAttachmentBuilders.forFile(file));
return this;
Expand Down Expand Up @@ -176,13 +180,16 @@ private void validateConfiguration() throws EmailException {
throw new EmailException("Cannot create email without recipients. Please provide at least one valid email address!");
}

if (StringUtils.isBlank(body) && StringUtils.isBlank(templateName)) {
if (StringUtils.isBlank(body) && StringUtils.isBlank(templateHtml)) {
throw new EmailException("Cannot create email without content. There must be a configuration for either body or template!");
}

if (StringUtils.isNotBlank(body) && StringUtils.isNotBlank(templateName)) {
if (StringUtils.isNotBlank(body) && StringUtils.isNotBlank(templateHtml)) {
throw new EmailException("Cannot create email without ambiguous content. There must be a configuration for either body or template, not both!");
}
if (templateHtml != null && templateLocale == null) {
throw new EmailException("Email creation failed: missing configuration for locale.");
}
}

private void configureAddresses(HtmlEmail htmlEmail) throws EmailException {
Expand All @@ -201,11 +208,11 @@ private void configureAddresses(HtmlEmail htmlEmail) throws EmailException {
}

private void processMessageContent(HtmlEmail htmlEmail, HtmlEmailGenerator htmlEmailGenerator) throws EmailException {
if (templateName != null && templateLocale != null) {
if (templateHtml != null) {
templateParameter("subject", subject);
templateParameter("recipients", emptyIfNull(toAddresses));
templateParameter("ccRecipients", emptyIfNull(ccAddresses));
body = htmlEmailGenerator.processTemplate(templateName, contextParameters, templateLocale);
body = htmlEmailGenerator.processTemplate(templateHtml, contextParameters, templateLocale);
}

htmlEmail.setSubject(subject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ default HtmlEmail createHtmlEmailFromTemplate(String subject, Collection<Interne
Map<String, Object> templateParameters, Locale locale) throws EmailException {
return newHtmlEmail()
.subject(subject)
.template(template, locale)
.template(template)
.locale(locale)
.templateParameter(templateParameters)
.custom(builder -> emptyIfNull(to).forEach(builder::to))
.custom(builder -> emptyIfNull(cc).forEach(builder::cc))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package me.cxdev.commerce.toolkit.email.dao;

import de.hybris.platform.servicelayer.exceptions.UnknownIdentifierException;

import me.cxdev.commerce.toolkit.model.ThymeleafEmailTemplateModel;

/**
* DAO for retrieving {@link ThymeleafEmailTemplateModel} instances from the database.
*
* <p>Implementations query the {@code ThymeleafEmailTemplate} type by its unique code
* using FlexibleSearch.
*/
public interface EmailTemplateResolverDao {
/**
* Searches for a {@link ThymeleafEmailTemplateModel} by its unique code.
*
* @param templateName the unique code of the template (e.g. {@code "contact"}, {@code "registration"})
* @return the matching {@link ThymeleafEmailTemplateModel}, never {@code null}
* @throws UnknownIdentifierException if no template with the given code exists in the database
*/
ThymeleafEmailTemplateModel searchTemplate(String templateName) throws UnknownIdentifierException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package me.cxdev.commerce.toolkit.email.dao.impl;

import de.hybris.platform.servicelayer.exceptions.UnknownIdentifierException;
import de.hybris.platform.servicelayer.search.FlexibleSearchQuery;
import de.hybris.platform.servicelayer.search.FlexibleSearchService;
import de.hybris.platform.servicelayer.search.SearchResult;

import org.apache.log4j.Logger;

import me.cxdev.commerce.toolkit.email.dao.EmailTemplateResolverDao;
import me.cxdev.commerce.toolkit.model.ThymeleafEmailTemplateModel;

public class DatabaseEmailTemplateResolverDao implements EmailTemplateResolverDao {

private static final Logger LOG = Logger.getLogger(DatabaseEmailTemplateResolverDao.class);

private final String QUERY = "SELECT {pk} FROM {ThymeleafEmailTemplate} WHERE {code} = ?templateName";
private final FlexibleSearchService flexibleSearchService;

public DatabaseEmailTemplateResolverDao(FlexibleSearchService flexibleSearchService) {
this.flexibleSearchService = flexibleSearchService;
}

public ThymeleafEmailTemplateModel searchTemplate(String templateName) throws UnknownIdentifierException {
FlexibleSearchQuery query = new FlexibleSearchQuery(QUERY);
query.addQueryParameter("templateName", templateName);
LOG.debug(String.format("Searching for email template with template name: %s", templateName));
final SearchResult<ThymeleafEmailTemplateModel> result = flexibleSearchService.search(query);
if (result.getResult().isEmpty()) {
throw new UnknownIdentifierException(String.format("No email template found for template name: %s", templateName));
}
return result.getResult().getFirst();
}
}
Loading
Loading