These libraries help you send an email but if you want to use a templated content, you will have to manually integrate a template engine.
These libraries also provide only implementations based on Java Mail API. But in some environments, you might NOT want to send the email through SMTP but to use an external service (like https://sendgrid.com/[SendGrid] for example). Furthermore, those libraries are bound by design to frameworks or libraries that you might not want to use in your own context.
So, now you would want to find a sending library with a high level of abstraction to avoid binding issues with any template engine, design framework or sender service... Is email the only possible message type ? No, so why not sending SMS, Tweet or anything the same way ?
=== The Ogham module
This module is designed for handling any kind of message the same way. It also provides several implementations for the same message type. It selects the best implementation based on the classpath or properties for example. You can easily add your own implementation.
It also provides templating support and integrates natively several template engines. You can also add your own.
It is framework and library agnostic and provides bridges for common frameworks integration (Spring, JSF, ...).
When using the module to send email based on an HTML template, the templating system let you design your HTML like a standard HTML page. It automatically transforms the associated resources (images, css files...) to be usable in an email context (automatic inline css, embed images...). You don't need to write your HTML specifically for email.
<1> Add Ogham starter for Spring Boot to benefit for all Ogham features in
your Spring Boot application.
<2> Import FreeMarker starter as usual. Ogham will adapt to additional FreeMarker
provided by Spring Boot.
<3> Import Thymeleaf starter as usual. Ogham will adapt to additional Thymeleaf
provided by Spring Boot.
<4> Import Mail starter as usual. Ogham will adapt to use mail features provided
by Spring Boot.
'''
[role=tab]
image:src/docs/resources/images/icons/gradlephant-logo.png[Gradle,width=114,height=30]
.build.gradle
[source, groovy, subs=attributes+, role="collapse-lines:1-13,21-27 highlight-lines:16"]
----
...
dependencies {
implementation 'fr.sii.ogham:ogham-spring-boot-starter-all:3.0.0' // <1>
implementation 'org.springframework.boot:spring-boot-starter-freemarker' // <2>
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // <3>
implementation 'org.springframework.boot:spring-boot-starter-mail' // <4>
...
}
----
<1> Add Ogham starter for Spring Boot to benefit for all Ogham features in
your Spring Boot application.
<2> Import FreeMarker starter as usual. Ogham will adapt to additional FreeMarker
provided by Spring Boot.
<3> Import Thymeleaf starter as usual. Ogham will adapt to additional Thymeleaf
provided by Spring Boot.
<4> Import Mail starter as usual. Ogham will adapt to use mail features provided
by Spring Boot.
_____
Ogham will auto-configure to use Spring Boot additions and support Spring Boot
configuration properties like `spring.mail.host` for example.
Ogham is compatible with following Spring Boot versions:
* 1.4.x (currently automatically tested against 1.4.7.RELEASE, see note below)
* 1.5.x (currently automatically tested against 1.5.22.RELEASE)
* 2.1.x (currently automatically tested against 2.1.18.RELEASE)
* 2.2.x (currently automatically tested against 2.2.12.RELEASE)
* 2.3.x (currently automatically tested against 2.3.7.RELEASE)
* 2.4.x (currently automatically tested against 2.4.1.RELEASE)
.Java version compatibility
[NOTE]
====
Ogham is compatible with Java 8 and up to Java 15 (included).
However, Spring Boot may not be compatible with some Java versions (depending on
Spring Boot version).
====
.Java 7
[IMPORTANT]
====
Java 7 support has been dropped since Ogham 3.0.0. Therefore, Ogham is no more
tested with Spring Boot 1.3.x
====
.Spring Boot 1.4.x and WireMock
[IMPORTANT]
====
Latest WireMock versions are not compatible with Spring Boot 1.4.x. So if you
are using Spring Boot 1.4.x, also using `ogham-test-utils` for writing tests and
want to use WireMock in one of your test, you may experience `ClassNotFoundException`.
This is due to different `org.apache.httpcomponents` versions.
In this case, just use a different version of WireMock by manually adding the dependency
`com.github.tomakehurst:wiremock-jre8:2.23.2:test`. This will force to use a
previous WireMock version that is compatible with Spring Boot 1.4.x.
====
==========
== Select the features you need
Importing `ogham-all` dependency or `ogham-spring-boot-starter-all` dependency
is easy but may import dependencies that you
don't need. For example, you may only need FreeMarker but not Thymeleaf. Or
you may only need to send emails through SMTP but never use SendGrid.
See how to https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#select-features-standalone[select features].
== Usage
[NOTE]
====
All samples with templates are using ThymeLeaf as template engine. For FreeMarker samples,
take a look at https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#freemarker[FreeMarker section].
====
=== Send Email
[NOTE]
====
The samples are available in the https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage[sample-standard-usage sub-project].
All samples shown bellow are using SMTP for sending email.
See https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#sendgrid[Sending email through SendGrid]
to know how to send email using SendGrid HTTP API.
====
==== First email using an existing SMTP server
:note-caption: :grey_question:
[NOTE]
====
This sample demonstrates a basic usage of Ogham. This sample doesn't
show real improvement compared to other libraries. However, it is a
the base for understanding the other samples.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
This sample shows how to send a basic email.
The first lines configure the properties that will be used by the sender.
Then you must create the service. You can use the MessagingBuilder to help you to create the service.
Finally, the last line sends the email. The specified email is really basic. It only contains the subject, the textual content and the receiver address. The sender address is automatically added to the email by the service based on configuration properties.
[role=tab-container no-max-height]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9 irrelevant-lines:1-9"]
----
...
public class BasicSample {
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.put("mail.smtp.host", "");
properties.put("mail.smtp.port", "");
properties.put("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard() // <1>
.environment()
.properties(properties) // <2>
.and()
.build(); // <3>
// send the email using fluent API
service.send(new Email() // <4>
.subject("BasicSample")
.body().string("email content")
.to("ogham-test@yopmail.com"));
}
}
----
<1> Use the standard builder (predefined behavior)
<2> Register the custom properties
<3> Create a MessagingService instance
<4> Send an email with a subject and a simple body. The sender address is automatically set using `ogham.email.from.default-value` property
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/BasicSample.java?ts=2[Source code of the sample].
_____
The construction of the email is done using a fluent API in order to chain calls and to have a more readable code.
Properties are directly provided in the code. You can instead https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#properties-handling[use a configuration file].
.Email address format
[TIP]
====
Ogham supports the personal information in email address. The format is `personal `.
For example, the address with personal `Ogham Test ` will result in
`ogham-test@yopmail.com` as email address and `Ogham Test` as personal.
====
==========
==== Use an HTML template for email body
:note-caption: :grey_question:
[NOTE]
====
This sample demonstrates templating integration to easily externalize
the email content from the source code.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
This sample shows how to send an email with a content following a template engine language.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9,34-46 irrelevant-lines:1-9,12-19 highlight-lines:28-29"]
----
...
public class HtmlTemplateSample {
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("mail.smtp.host", "");
properties.setProperty("mail.smtp.port", "");
properties.setProperty("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard() // <1>
.environment()
.properties(properties) // <2>
.and()
.build(); // <3>
// send the email using fluent API
service.send(new Email() // <4>
.subject("HtmlTemplateSample")
.body().template("classpath:/template/thymeleaf/simple.html", // <5>
new SimpleBean("foo", 42)) // <6>
.to("ogham-test@yopmail.com"));
}
public static class SimpleBean {
...
}
}
----
<1> Use the standard builder (predefined behavior)
<2> Register the custom properties
<3> Create a MessagingService instance
<4> Send an email with an explicit subject. The sender address is automatically set using `ogham.email.from.default-value` property
<5> Indicate the path to the HTML template file (in the classpath)
<6> Use any bean object for replacing variables in template
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/HtmlTemplateSample.java?ts=2[Source code of the sample].
'''
[role=tab]
image:src/docs/resources/images/icons/thymeleaf.jpg[width=30,height=30] ThymeLeaf template
[source, html]
----
----
<1> Include the ThymeLeaf namespace
<2> Use the `name` attribute value in the template
<3> Use the `value` attribute value in the template
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/thymeleaf/simple.html?ts=2[Source code of the HTML template]
_____
Using a template is straightforward. Instead of providing a string as body (using `body().string(...)`),
you change to `body().template(..., ...)`.
The `template` method requires two pieces of information:
* The path to the template
* The variables to evaluate in the template
The path to the template is a string that may contain a *lookup* prefix. The lookup prefix is used to indicate
where to search the template (from file system, from classpath or anywhere else). Here we explicitly
ask to load the template from classpath (using prefix `classpath:`). If no lookup is defined,
classpath is used by default. See https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#resource-resolution[Resource resolution section] for more information.
The variables are any object you are using in your application. No need to convert your object to a
particular format. Directly use what you want.
==========
==== Use HTML title as email subject
:note-caption: :grey_question:
[NOTE]
====
This sample demonstrates that you can use a single source for both the body and
the subject (same variables and same context). This is particularly useful
for internationalization.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
This sample is a variant of the previous one. It allows you to
directly use the HTML title as subject of your email. It may be
useful to use variables in the subject too, to mutualize the code
and to avoid to create a new file or to use
a different evaluation syntax or context just for one line.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-10,35-47 irrelevant-lines:1-10,12-19"]
----
...
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("mail.smtp.host", "");
properties.setProperty("mail.smtp.port", "");
properties.setProperty("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard()
.environment()
.properties(properties)
.and()
.build();
// send the email using fluent API (do not specify subject)
// subject is set to null to let automatic mechanism to read the title
// of the HTML and use it as subject of your email
service.send(new Email() // <1>
.body().template("classpath:/template/thymeleaf/simpleWithSubject.html",
new SimpleBean("foo", 42))
.to("ogham-test@yopmail.com"));
}
public static class SimpleBean {
...
}
}
----
<1> Subject is no more in Java code
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/HtmlTemplateWithSubjectSample.java?ts=2[Source code of the sample]
'''
[role=tab]
image:src/docs/resources/images/icons/thymeleaf.jpg[width=30,height=30] ThymeLeaf template
[source, html, role="highlight-lines:4"]
----
Subject of the email - [[${name}]]
----
<1> The subject is defined in the template and can use same evaluation context (`SimpleBean`).
NOTE: The subject of the email will be `Subject of the email - Welcome foo !`
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/thymeleaf/simpleWithSubject.html?ts=2[Source code of the HTML template]
_____
For text templates, the subject is automatically used (like for HTML title) if the first line starts
with `Subject:` (spaces can be added after colon). Other lines are used as content of the email.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9,35-47 irrelevant-lines:1-9,12-19"]
----
...
public class TextTemplateWithSubjectSample {
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("mail.smtp.host", "");
properties.setProperty("mail.smtp.port", "");
properties.setProperty("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard()
.environment()
.properties(properties)
.and()
.build();
// send the email using fluent API (do not specify subject)
// subject is set to null to let automatic mechanism to read the title
// of the first line if prefixed by "Subject:" and use it as subject of your email
service.send(new Email() // <1>
.body().template("classpath:/template/freemarker/simpleWithSubject.txt.ftl",
new SimpleBean("foo", 42))
.to("ogham-test@yopmail.com"));
}
public static class SimpleBean {
...
}
}
----
<1> Subject is no more in Java code
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/TextTemplateWithSubjectSample.java?ts=2[Source code of the sample]
'''
[role=tab]
image:src/docs/resources/images/icons/freemarker-logo.png[width=60,height=24] Text template
[source, text, role="highlight-lines:1"]
----
Subject: Welcome ${name} !
Hello ${name},
Foo bar ${value}
----
NOTE: The subject of the email will be `Welcome foo !`
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/freemarker/simpleWithSubject.txt.ftl?ts=2[Source code of the text template]
_____
==========
==== HTML body with CSS and images
:note-caption: :grey_question:
[NOTE]
====
This sample demonstrates that you can reference resources like CSS files or
images as usual without worrying about technical concerns and limitations for
such resources in email context.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
When you develop a Web application, you can use HTML for the content and CSS for
layout and theming. HTML and CSS can use images to make a beautiful Web page.
Each concern is separated in a different file. This is a good practice.
However, writing an HTML email is totally different. Indeed, email clients are
not as evolved as Web browsers. Even worse, some clients disable some features
on purpose (like GMail that prevents using `style` tag). To make an email work
on several clients, you should follow these rules:
* `` tags that use local images must be embedded
* Use XHTML instead of HTML
* Remove HTML comments (except conditional comments used to target Outlook)
* Add border=0 on all images to avoid an ugly border
* Do not write shortcut CSS values (`padding: 4px 4px 4px 4px;` instead of
`padding: 4px`)
* Padding is not supported on some clients so you must use margins instead
(adding a parent just for the layout)
* Background images on body should be moved on another node
* CSS3 properties are not supported
* Images must have `alt` attribute
* ...
There are many other rules but the developer should not be constrained and
should be able to write its HTML and CSS like as usual in Web browsers. Ogham
simplifies image and CSS integration and is able to partially rewrite the HTML.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-10,36-48 irrelevant-lines:1-10,12-19 highlight-lines:30-31"]
----
...
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("mail.smtp.host", "");
properties.setProperty("mail.smtp.port", "");
properties.setProperty("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard()
.environment()
.properties(properties)
.and()
.build();
// send the email using fluent API
// Note that the extension of the template is not given. This version
// automatically takes the provided path and adds the '.html' extension
// for the HTML template and '.txt.ftl' for text template
service.send(new Email()
.body().template("classpath:/template/withImagesAndCss/resources", // <1>
new SimpleBean("foo", 42)) // <2>
.to("ogham-test@yopmail.com"));
}
public static class SimpleBean {
...
}
}
----
<1> The path to the templates (`/template/withImagesAndCss/resources.html` for the main body,
`/template/withImagesAndCss/resources.txt.ftl` for the text alternative)
<2> The template context
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/HtmlWithImagesAndCssTemplateSample.java?ts=2[Source code of the sample]
'''
[role=tab]
image:src/docs/resources/images/icons/thymeleaf-html.jpg[width=30,height=30] ThymeLeaf template
[source, html, role="highlight-lines:7-8,12,18,27,39,42"]
----
HtmlWithImagesAndCssTemplateSample
name
value
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In
tempus adipiscing felis, sit amet blandit ipsum volutpat sed. Morbi
porttitor, eget accumsan dictum, nisi libero ultricies ipsum, in
posuere mauris neque at erat.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In
tempus adipiscing felis, sit amet blandit ipsum volutpat sed. Morbi
porttitor, eget accumsan dictum, nisi libero ultricies ipsum, in
posuere mauris neque at erat.
----
<1> The CSS is parsed by Ogham and applied directly on the HTML (using `style` attribute)
<2> The CSS is parsed by Ogham and applied directly on the HTML (using `style` attribute). The CSS may
contain rules that override some rules of other CSS files (like in a browser)
<3> The image is automatically embedded (the path is replaced by a
https://tools.ietf.org/html/rfc4021#section-2.2.2[Content-ID (or CID)] and the image is attached
to the email using `ContentDisposition.INLINE` with the Content-ID header). The content type is
automatically determined
<4> The image is converted to a base64 string. The `src` attribute of the image is updated using
https://en.wikipedia.org/wiki/Data_URI_scheme[data URI scheme]. The content type is automatically
determined
<5> Same as <3>
<6> Same as <3>
<7> The image is not inlined by Ogham. This can be useful to embed it manually.
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/withImagesAndCss/resources.html?ts=2[Source code of the HTML template]
'''
[role=tab]
image:src/docs/resources/images/icons/css.png[width=37,height=30] CSS
.layout.css
[source, css]
----
body {
margin: 10px auto 30px auto;
width: 600px;
}
header {
padding: 40px 30px;
}
header img {
display: block;
margin: auto;
}
.content {
padding: 40px 30px;
}
.left,
.right {
width: 250px;
display: inline-block;
}
.left {
margin-right: 36px;
}
footer {
padding: 30px;
}
footer .sender-info,
footer .social {
text-align: center;
}
footer .social a {
display: inline-block;
}
footer .sender-info {
margin-top: 20px;
}
----
.theme.css
[source, css]
----
body {
border: 1px solid #ccc;
}
header {
background: #70bbd9;
}
.content {
text-align: justify;
}
footer {
color:#ffffff;
font-family:Arial, sans-serif;
font-size:14px;
background: #ee4c50;
}
footer .social a {
color:#ffffff;
}
.white {
color: #fff;
}
----
'''
[role=tab]
image:src/docs/resources/images/icons/html.png[width=37,height=30] Sent HTML
_____
Here is the list of supported transformations:
* [x] `` tags that use local images are embedded (using `cid` reference)
* [x] `` tags that use local images are embedded (using base64 data URI)
* [x] Inline CSS rules using `style` attribute
* [x] `background` images that use local images are embedded (using `cid` reference)
* [x] `background` images that use local images are embedded (using base64 data URI)
* [ ] Use XHTML instead of HTML
* [ ] Tables used for layout explicitly set default values
* [ ] Remove HTML comments (except conditional comments used to target Outlook)
* [ ] Add border=0 on all images to avoid an ugly border
* [ ] Do not write shortcut CSS values (`padding: 4px 4px 4px 4px;` instead of `padding: 4px`)
* [ ] Padding is not supported on some clients so you must use margins instead (adding a parent just for the layout)
* [ ] Background images on body should be moved on another node
* [ ] Images must have `alt` attribute
==========
==== A working preview of the HTML body (text alternative)
:note-caption: :grey_question:
[NOTE]
====
Have you never received an email with strange characters in a textual preview ?
This sample demonstrates how to easily provide a text alternative that is
used as textual preview.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
Sending an email with HTML content **and** text content might be really important, at least for
smartphones. When a smartphone receives an email, it displays the sender, the subject and also a
preview of the message, using the text alternative. If the message is only HTML, the preview might
be unreadable.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9 irrelevant-lines:1-9,11-20,23-30 highlight-lines:39-40"]
----
...
public class HtmlAndTextSample {
private static String html = ""
+ ""
+ ""
+ ""
+ "
Hello World
"
+ "
Foo bar
"
+ ""
+ "";
private static String text = "Hello World !\r\n"
+ "Foo bar";
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.put("mail.smtp.host", "");
properties.put("mail.smtp.port", "");
properties.put("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard()
.environment()
.properties(properties)
.and()
.build();
// send the email using the fluent API
service.send(new Email()
.subject("HtmlAndTextSample")
.text().string(text) // <1>
.html().string(html) // <2>
.to("ogham-test@yopmail.com"));
}
}
----
<1> Explicitly set the textual content (used as alternative body). The alternative body is used when
the email client doesn't support HTML or as a preview of the email.
<2> Explicitly set the HTML content (used as main body)
[NOTE]
=====
The call order between `text()` and `html()` doesn't matter (unlike using `.content(new MultiContent(...))`).
=====
[NOTE]
=====
The underlying `Content` is a `MultiContent`.
=====
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/HtmlAndTextSample.java?ts=2[Source code of the sample]
_____
Obviously, you can use templates too. Even better, the following sample shows the shorthand version
that avoids specifying twice the path to the templates (a single path without extension for both HTML
and text template files).
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9,37-49 irrelevant-lines:1-9,12-19 highlight-lines:31-32"]
----
...
public class HtmlAndTextTemplateSample {
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("mail.smtp.host", "");
properties.setProperty("mail.smtp.port", "");
properties.setProperty("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard()
.environment()
.properties(properties)
.and()
.build();
// send the email using fluent API
// Note that the extension of the template is not given. This version
// automatically takes the provided path and adds the '.html' extension
// for the HTML template and '.txt' for text template
service.send(new Email()
.subject("HtmlAndTextTemplateSample")
.body().template("classpath:/template/thymeleaf/simple", // <1>
new SimpleBean("foo", 42)) // <2>
.to("ogham-test@yopmail.com"));
}
public static class SimpleBean {
...
}
}
----
<1> The body contains two parts (main body and alternative body) because there are two templates (one
for HTML located at `/template/thymeleaf/simple.html` and one for text located at
`/template/thymeleaf/simple.txt`). Only a single path is specified for both template files (without
extension).
<2> The object used for evaluation as usual when using templates (same object used for both HTML and text)
[NOTE]
=====
The underlying `Content` is a `MultiTemplateContent`.
=====
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/HtmlAndTextTemplateSample.java?ts=2[Source code of the sample]
'''
[role=tab]
image:src/docs/resources/images/icons/thymeleaf-text.jpg[width=30,height=30] Text template
.Text template located in `src/main/resources/template/thymeleaf/simple.txt`
[source, txt]
----
[[${name}]] [[${value}]]
----
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/thymeleaf/simple.txt?ts=2[Source code of the text template]
'''
[role=tab]
image:src/docs/resources/images/icons/thymeleaf-html.jpg[width=30,height=30] HTML template
.HTML template located at `src/main/resources/template/thymeleaf/simple.html`
[source, html]
----
----
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/thymeleaf/simple.html?ts=2[Source code of the HTML template]
_____
Ogham will automatically determine file extensions to append according to the kind of message you
are sending. For email, Ogham will search a HTML and a text file by default:
* Using ThymeLeaf, the file extensions are `.html` and `.txt` (configurable).
* Using FreeMarker, Ogham will search files with extensions `.html.ftl` and `.txt.ftl` (configurable).
If you are using `body()` (or explicitly using `content(new MultiTemplateContent(...))`) and you only
provide one template (only `HTML` for example). Ogham will not fail by default (configurable).
Therefore, you can start your code with only a HTML template and add the text template later when you
need it. That way, your Java code doesn't require any change.
It is possible to mix templates in the same application. Even better, you can use a template engine
that is better suited for HTML like Thymeleaf and FreeMarker that is better for textual version for
the same email. Just write your templates with the engine you want.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9,37-49 irrelevant-lines:1-9,12-19 highlight-lines:31-32"]
----
...
public class HtmlAndTextMixedTemplateEnginesSample {
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("mail.smtp.host", "");
properties.setProperty("mail.smtp.port", "");
properties.setProperty("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard()
.environment()
.properties(properties)
.and()
.build();
// send the email using fluent API
// Note that the extension of the template is not given. This version
// automatically takes the provided path and adds the '.html' extension
// for the HTML template and '.txt.ftl' for text template
service.send(new Email()
.subject("HtmlAndTextMixedTemplateEnginesSample")
.body().template("classpath:/template/mixed/simple", // <1>
new SimpleBean("foo", 42)) // <2>
.to("ogham-test@yopmail.com"));
}
public static class SimpleBean {
...
}
}
----
<1> The body contains two parts (main body and alternative body) because there are two templates (one
for HTML located at `/template/thymeleaf/simple.html` and one for text located at
`/template/thymeleaf/simple.txt.ftl`). Only a single path is specified for both template files (without
extension). The HTML template uses Thymeleaf while the text template uses FreeMarker.
<2> The object used for evaluation as usual when using templates (same object used for both HTML and text)
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/HtmlAndTextMixedTemplateEnginesSample.java?ts=2[Source code of the sample]
'''
[role=tab]
image:src/docs/resources/images/icons/freemarker-logo.png[width=60,height=24] Text template
.Text template located in `src/main/resources/template/mixed/simple.txt.ftl`
[source, txt]
----
${name} ${value}
----
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/mixed/simple.txt.ftl?ts=2[Source code of the text template]
'''
[role=tab]
image:src/docs/resources/images/icons/thymeleaf.jpg[width=30,height=30] HTML template
.HTML template located at `src/main/resources/template/mixed/simple.html`
[source, html]
----
----
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/mixed/simple.html?ts=2[Source code of the HTML template]
_____
You can notice that the Java code has not changed at all (only the path for the sample). The aim is
to use the template engine that best suits your needs.
==========
==== Attach files to the email
:note-caption: :grey_question:
[NOTE]
====
This sample demonstrates how to send files with the email.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-12 irrelevant-lines:1-12,14-21 highlight-lines:32-33"]
----
...
public static void main(String[] args) throws MessagingException, IOException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.put("mail.smtp.host", "");
properties.put("mail.smtp.port", "");
properties.put("ogham.email.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard()
.environment()
.properties(properties)
.and()
.build();
// send the email using fluent API
service.send(new Email()
.subject("WithAttachmentSample")
.body().string("content of the email")
.to("ogham-test@yopmail.com")
.attach().resource("classpath:/attachment/test.pdf") // <1>
.attach().stream("from-stream.pdf", loadInputStream())); // <2>
}
private static InputStream loadInputStream() {
return WithAttachmentSample.class.getResourceAsStream("/attachment/test.pdf");
}
}
----
<1> Attach a PDF file that exists in the classpath to the email. The name of the attachment uses the name of the file
<2> Use an `InputStream` and name the attachment
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/email/WithAttachmentSample.java?ts=2[Source code of the sample]
_____
Attaching a file to the email is quite simple. You just need to provide the path to the file.
The file is loaded from classpath but could also be loaded from file system or anywhere else
(see https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#resource-resolution[ resource resolution section]). In case you are using a file,
the name of the attachment displayed in the email is automatically determined (`test.pdf` in the example).
It is often not possible to handle files directly. In that case you will use `InputStream` or
`byte[]`. In that case, you need to name the attachment explicitly.
In both cases, the mimetype is automatically determined (`application/pdf` in this case).
Mimetype is really important to ensure that the recipient(s) will be able to download or view
the files correctly. It is possible to explicitly set the content type of the attachment if the
automatic behavior doesn't fit your needs.
The file content is linked to the email using `ContentDisposition.ATTACHMENT`.
IMPORTANT: If you are using `InputStream`, you need to close the stream after sending the email.
TIP: You can also add a custom description for any attachment but in this case use `Attachment` class.
[TIP]
====
You can link a file to the email body using `embed()` instead of `attach()`. To make the link,
the email body must contain an explicit reference using a
https://tools.ietf.org/html/rfc4021#section-2.2.2[Content-ID (or CID)]. The linked attachment
must provide the Content-ID header with the same CID (automatically set by Ogham). The embedded attachment
disposition is automatically set to `ContentDisposition.INLINE`.
====
==========
==== Other email features
See user guide to read about https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#email-usage[the many other features].
=== Send SMS
[NOTE]
====
The samples are available in the https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage[sample-standard-usage sub-project].
All samples shown bellow are using SMPP for sending SMS.
The https://en.wikipedia.org/wiki/Short_Message_Peer-to-Peer[SMPP] protocol
is the standard way to send SMS. Only a subset of SMPP properties are used
in following samples. The whole list of SMPP properties is available
in https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#advanced-smpp-configuration[advanced configuration].
See https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#ovh[Sending SMS through OVH] to know how to send SMS using OVH HTTP API.
====
==== First SMS using an existing SMPP server
:note-caption: :grey_question:
[NOTE]
====
This sample demonstrates a basic usage of Ogham for sending SMS. It is a
the base for understanding the other samples.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
This sample defines two properties mandatory (system ID and password) by this protocol in order to use it.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9 irrelevant-lines:1-9"]
----
...
public class BasicSample {
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("ogham.sms.smpp.host", ""); // <1>
properties.setProperty("ogham.sms.smpp.port", ""); // <2>
properties.setProperty("ogham.sms.smpp.system-id", ""); // <3>
properties.setProperty("ogham.sms.smpp.password", ""); // <4>
properties.setProperty("ogham.sms.from.default-value", ""); // <5>
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard() // <6>
.environment()
.properties(properties) // <7>
.and()
.build(); // <8>
// send the sms using fluent API
service.send(new Sms() // <9>
.message().string("sms content")
.to("+33752962193"));
}
}
----
<1> Configure the SMPP server host
<2> Configure the SMPP server port
<3> The SMPP system ID
<4> The SMPP password
<5> The phone number of the sender
<6> Use the standard builder (predefined behavior)
<7> Register the custom properties
<8> Create a MessagingService instance
<9> Send a SMS with a simple message. The sender phone number is automatically set using
`ogham.sms.from.default-value` property
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/sms/BasicSample.java?ts=2[Source code of the sample].
_____
The construction of the SMS is done using a fluent API in order to chain calls and to have a more
readable code.
Properties are directly provided in the code. You can instead https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#properties-handling[use a configuration file].
==========
==== Use a template for SMS content
:note-caption: :grey_question:
[NOTE]
====
This sample demonstrates templating integration to easily externalize
the SMS content from the source code.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
This sample shows how to send a SMS with a content following a template engine language.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9,35-47 irrelevant-lines:1-9,12-21 highlight-lines:29,30"]
----
...
public class TemplateSample {
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("ogham.sms.smpp.host", "");
properties.setProperty("ogham.sms.smpp.port", "");
properties.setProperty("ogham.sms.smpp.system-id", "");
properties.setProperty("ogham.sms.smpp.password", "");
properties.setProperty("ogham.sms.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard() // <1>
.environment()
.properties(properties) // <2>
.and()
.build(); // <3>
// send the sms using fluent API
service.send(new Sms() // <4>
.message().template("classpath:/template/thymeleaf/simple.txt", // <5>
new SimpleBean("foo", 42)) // <6>
.to("+33752962193"));
}
public static class SimpleBean {
...
}
}
----
<1> Use the standard builder (predefined behavior)
<2> Register the custom properties
<3> Create a MessagingService instance
<4> Send a SMS with message that comes from the evaluated template. The sender address is automatically
set using `ogham.sms.from.default-value` property
<5> Indicate the path to the template file (in the classpath)
<6> Use any bean object for replacing variables in template
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/sms/TemplateSample.java?ts=2[Source code of the sample].
'''
[role=tab]
image:src/docs/resources/images/icons/thymeleaf.jpg[width=30,height=30] ThymeLeaf template
[source, html]
----
[[${name}]] [[${value}]]
----
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/resources/template/thymeleaf/simple.txt?ts=2[Source code of the template]
_____
Using a template is straightforward. Instead of providing a string content (using `.message().string(...)`),
you call `.message().template(..., ...)`.
The `template` method requires two pieces of information:
* The path to the template
* The variables to evaluate in the template
The path to the template is a string with a *lookup* prefix. The lookup prefix is used to indicate
where to search the template (from file system, from classpath or anywhere else). Here we explicitly
ask to load the template from classpath (using prefix `classpath:`). If no lookup is defined,
classpath is used by default. See https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#resource-resolution[Resource resolution section] for more information.
The variables are any object you are using in your application. No need to convert your object to
a particular format. Directly use what you want.
==========
==== Send a long SMS
:note-caption: :grey_question:
[NOTE]
====
This sample demonstrates that you don't have to handle these kind of
technical concerns.
====
:note-caption: :information_source:
.Explanations & sample
[%collapsible]
==========
As you may know, SMS stands for Short Message Service. Basically, the messages are limited to a maximum
of 160 characters if character encoding is using 7bits. Using a 8-bit character encoding decreases
the limit to 140 characters and 70 characters for a 16-bit encoding. If needed, the library will
split your messages into several parts the right way to be recomposed by clients later (according
to the message encoding). Therefore, you don't need to handle the split of messages in your code:
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-9 irrelevant-lines:1-9,12-21 highlight-lines:27-30"]
----
...
public class LongMessageSample {
public static void main(String[] args) throws MessagingException {
// configure properties (could be stored in a properties file or defined
// in System properties)
Properties properties = new Properties();
properties.setProperty("ogham.sms.smpp.host", "");
properties.setProperty("ogham.sms.smpp.port", "");
properties.setProperty("ogham.sms.smpp.system-id", "");
properties.setProperty("ogham.sms.smpp.password", "");
properties.setProperty("ogham.sms.from.default-value", "");
// Instantiate the messaging service using default behavior and
// provided properties
MessagingService service = MessagingBuilder.standard()
.environment()
.properties(properties)
.and()
.build();
String longMessage = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad "
+ "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehender"
+ "it in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui o"
+ "fficia deserunt mollit anim id est laborum.";
// send the sms using fluent API
service.send(new Sms()
.message().string(longMessage)
.to("+33752962193"));
}
}
----
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/main/java/fr/sii/ogham/sample/standard/sms/LongMessageSample.java?ts=2[Source code of the sample].
_____
.Message length depends on encoding
[NOTE]
====
Larger content (concatenated SMS, multipart or segmented SMS, or "long SMS") can be sent using
multiple messages, in which case each message will start with a User Data Header (UDH) containing
segmentation information. Since UDH is part of the payload, the number of available characters per
segment is lower: 153 for 7-bit encoding, 134 for 8-bit encoding and 67 for 16-bit encoding. The
receiving handset is then responsible for reassembling the message and presenting it to the user
as one long message. While the standard theoretically permits up to 255 segments, 6 to 8 segment
messages are the practical maximum.
====
.Default encoding
[NOTE]
====
By default, Ogham sends the SMS using 8-bit encoding.
====
==========
==== Other SMS features
Ogham provides https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#sms-usage[many other features].
== Testing
=== Include test utilities
.Getting started
[%collapsible]
==========
[role=tab-container]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/maven-logo.png[Maven,width=118,height=30]
.Add `ogham-test-utils` dependency to your pom.xml
[source, xml, subs=attributes+]
----
fr.sii.oghamogham-test-utils3.0.0test
----
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/pom.xml?ts=2[View code of an example pom.xml]
'''
[role=tab]
image:src/docs/resources/images/icons/gradlephant-logo.png[Gradle,width=114,height=30]
.Add `ogham-test-utils` to your build.gradle
[source, groovy, subs=attributes+]
----
dependencies {
testImplementation 'fr.sii.ogham:ogham-test-utils:3.0.0'
}
----
_____
This will include:
* http://junit.org/junit5/[JUnit to run tests (JUnit 4 and Junit 5)]
* http://site.mockito.org/[Mockito for mocking Java code]
* http://hamcrest.org/JavaHamcrest/[Hamcrest for assertions]
* http://www.icegreen.com/greenmail/[GreenMail for mocking a SMTP server]
* https://jsmpp.org/[jSMPP for mocking a SMPP server]
* http://wiremock.org/[WireMock for mocking HTTP server]
* http://spockframework.org/[Spockframework for writing very very readable unit tests]
* and some utilities that may be useful
[TIP]
====
You can
https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#testing-tools-eclipse-static-imports[configure Eclipse]
to provide autocompletion for static imports.
====
==========
=== Testing emails
.Explanations & sample
[%collapsible]
==========
To test your application emails, you can start a local SMTP server. You can then use Ogham to make
assertions on your email (right recipients, right sender, right body...). Ogham uses
http://www.icegreen.com/greenmail/[GreenMail] as local SMTP server.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-23 irrelevant-lines:1-23"]
----
...
public class EmailTestSample {
private MessagingService oghamService;
@Rule
public final GreenMailRule greenMail = new GreenMailRule(SMTP); // <1>
@Before
public void setUp() throws IOException {
oghamService = MessagingBuilder.standard()
.environment()
.properties()
.set("ogham.email.from.default-value", "Sender Name ")
.set("mail.smtp.host", SMTP.getBindAddress()) // <2>
.set("mail.smtp.port", String.valueOf(SMTP.getPort())) // <3>
.and()
.and()
.build();
}
@Test
public void simple() throws MessagingException, javax.mail.MessagingException {
// @formatter:off
oghamService.send(new Email()
.subject("Simple")
.body().string("string body")
.to("Recipient Name "));
assertThat(greenMail).receivedMessages() // <4>
.count(is(1)) // <5>
.message(0) // <6>
.subject(is("Simple")) // <7>
.from()
.address(hasItems("test.sender@sii.fr")) // <8>
.personal(hasItems("Sender Name")).and() // <9>
.to()
.address(hasItems("recipient@sii.fr")) // <10>
.personal(hasItems("Recipient Name")).and() // <11>
.body()
.contentAsString(is("string body")) // <12>
.contentType(startsWith("text/plain")).and() // <13>
.alternative(nullValue()) // <14>
.attachments(emptyIterable()); // <15>
// @formatter:on
}
}
----
<1> Declare and initialize the GreenMail JUnit rule to start a local SMTP server
<2> Get the local SMTP server host address and configure Ogham to use this value
<3> Get the local SMTP server port and configure Ogham to use this value
<4> Entry point for declaring assertion on received emails using a fluent API
<5> Assert that one and only one email has been received
<6> Access the first received message for declaring assertions for that message using fluent API
<7> Assert that the subject of the first message is exactly `Simple` string
<8> Assert that the sender email address is exactly `test.sender@sii.fr`
<9> Assert that the sender name is exactly `Sender Name`
<10> Assert that the recipient email address is exactly `recipient@sii.fr`
<11> Assert that the recipient name is exactly `Recipient Name`
<12> Assert that the body of the received email is exactly `string body`
<13> Assert that the mimetype of the body of the received email starts with `text/plain`
<14> Assert that received email has no alternative content
<15> Assert that received email has no attachment
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/test/java/fr/sii/ogham/sample/test/EmailTestSample.java?ts=2[Source code of the sample].
_____
==========
=== Testing SMS
.Explanations & sample
[%collapsible]
==========
To test your application SMS, you can start a local SMPP server. You can then use Ogham to make assertions on you SMS (recipient phone number, sender phone number and message). Ogham uses https://jsmpp.org/[jSMPP] as local SMPP server.
[role="tab-container no-max-height"]
_____
'''
[role=tab]
image:src/docs/resources/images/icons/java-logo.png[width=16,height=30] Java
[source, java, role="collapse-lines:1-19 irrelevant-lines:1-19"]
----
...
public class SmsTestSample {
private MessagingService oghamService;
@Rule
public final SmppServerRule smppServer = new JsmppServerRule(); // <1>
@Before
public void setUp() throws IOException {
oghamService = MessagingBuilder.standard()
.environment()
.properties()
.set("ogham.sms.from.default-value", "+33603040506")
.set("ogham.sms.smpp.host", "localhost") // <2>
.set("ogham.sms.smpp.port", smppServer.getPort()) // <3>
.and()
.and()
.build();
}
@Test
public void simple() throws MessagingException {
// @formatter:off
oghamService.send(new Sms()
.message().string("sms content")
.to("0601020304"));
assertThat(smppServer).receivedMessages() // <4>
.count(is(1)) // <5>
.message(0) // <6>
.content(is("sms content")) // <7>
.from()
.number(is("+33603040506")) // <8>
.and()
.to()
.number(is("0601020304")); // <9>
// @formatter:on
}
}
----
<1> Declare and initialize the JUnit rule that encapsulates jSMPP for starting a local SMPP server in tests (you can set a different port than the default one)
<2> Configure Ogham to use `localhost` for SMPP host
<3> Get the local SMPP server port and configure Ogham to use this value
<4> Entry point for declaring assertion on received SMS using a fluent API
<5> Assert that one and only one SMS has been received
<6> Access the first received message for declaring assertions for that message using fluent API
<7> Assert that the received message text is exactly `sms content`
<8> Assert that the sender phone number is `+33603040506`
<9> Assert that the recipient phone number is `0601020304`
https://github.com/groupe-sii/ogham/tree/master/sample-standard-usage/src/test/java/fr/sii/ogham/sample/test/SmsTestSample.java?ts=2[Source code of the sample].
_____
==========
=== More testing features
Ogham provides many other tool features:
* https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#testing-email[Email assertions and tools].
* https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#testing-sms[SMS assertions and tools].
* https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#random-ports[Use random ports to parallelize tests].
== Advanced configuration
https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#section-advanced-configuration[See advanced configuration in full documentation]
== Extend
https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#section-extend[See how to extend Ogham in full documentation]
== Tips
https://groupe-sii.github.io/ogham/v3.0.0/user-manual.html#section-tips[See tips in full documentation]
== Support developer
I am currently the single developer on this project for several years.
I have many ideas for this project to ease other developer work.
You can help me by either https://groupe-sii.github.io/ogham/v3.0.0/contribute.html[contributing] or by https://groupe-sii.github.io/ogham/v3.0.0/donate.html[making a donation].
image:src/docs/resources/images/donation/paypal-small.png[alt="Donate with PayPal", link="https://www.paypal.com/donate?hosted_button_id=KQKEFU5LE5K2J"]
image:src/docs/resources/images/donation/coinbase-small.png[alt="Donate crypto (Bitcoin, Ethereum, ...)", link="https://commerce.coinbase.com/checkout/7c322363-b8ef-4624-8957-a750b631f859"]
Thank you !