Ogham User Manual

Table of Contents

1. Introduction

1.1. Existing libraries

Several libraries for sending email already exist:

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 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 ?

1.2. 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.2.1. Code once, adapt to environment

You write your business code once. Ogham handles environment switching just by providing different configuration:

Development

In development phase, you may want to send email through any SMTP server (GMail for example) and SMS through any SMPP server (SmsGlobal for example). Your templates can be embedded in classpath.

diag 4d054048ef810aadd6180fe4ba2ef667
Tests

You can test your emails content through an embedded local SMTP. You can do the same with SMS through an embedded local SMPP server.

Ogham directly provides embedded servers for testing. Your templates can be embedded in test sources.

diag 8385a83b1fbd518b9455ee2564ba8a34
Production

In production, your enterprise may use external services for sending emails or SMS (like SendGrid HTTP API for emails and OVH HTTP API for SMS).

Externalizing templates let the possibility to update them without needing to deploy the application again.

diag e1248c0af6c5ff8753a903b803e002e2

In all cases, the code never changes. Only Ogham configuration differs.

2. Quick start

2.1. Quick and simple inclusion

2.1.1. Standalone

Maven
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>sample</groupId>
  <artifactId>all</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <properties>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-all</artifactId>
      <version>3.0.0</version>
    </dependency>
  </dependencies>
</project>
Gradle
build.gradle
plugins {
    id 'java'
}

repositories {
  mavenLocal()
  mavenCentral()
}

dependencies {
  implementation 'fr.sii.ogham:ogham-all:3.0.0'
}

This will include:

  • Sending email through SMTP server (using Jakarta Mail, formely JavaMail)

  • Sending email through SendGrid

  • Sending SMS through SMPP server (using Cloudhopper)

  • Sending SMS through OVH SMS API

  • FreeMarker template engine available for building message contents

  • ThymeLeaf template engine available for building message contents

Java version compatibility

Ogham is compatible with Java 8 and up to Java 15 (included).

2.1.2. With Spring Boot

Maven
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>samples</groupId>
  <artifactId>ogham-and-spring-boot</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>ogham-and-spring-boot</name>
  <description>Demo project for Spring Boot</description>

  <properties>
    <java.version>11</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-spring-boot-starter-all</artifactId>
      <version>3.0.0</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
Gradle
build.gradle
plugins {
  id 'org.springframework.boot' version '2.3.3.RELEASE'
  id 'io.spring.dependency-management' version '1.0.10.RELEASE'
  id 'java'
}

group = 'samples'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {
  implementation 'fr.sii.ogham:ogham-spring-boot-starter-all:3.0.0'

  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}

test {
  useJUnitPlatform()
}

This will include:

  • Sending email through SMTP server (using JavaMail)

  • Sending email through SendGrid

  • Sending SMS through SMPP server (using Cloudhopper)

  • Sending SMS through OVH SMS API

  • FreeMarker template engine available for building message contents

  • ThymeLeaf template engine available for building message contents

  • Support of Spring Boot auto-detection mechanism and configuration properties

You can combine Ogham with existing Spring Boot dependencies:

Example 1. Ogham adapts itself to Spring Boot features
Maven
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>samples</groupId>
  <artifactId>ogham-and-spring-boot</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>ogham-and-spring-boot</name>
  <description>Demo project for Spring Boot</description>

  <properties>
    <java.version>11</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-spring-boot-starter-all</artifactId>    (1)
      <version>3.0.0</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-freemarker</artifactId>   (2)
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>    (3)
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-mail</artifactId>         (4)
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
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.
Gradle
build.gradle
plugins {
  id 'org.springframework.boot' version '2.3.3.RELEASE'
  id 'io.spring.dependency-management' version '1.0.10.RELEASE'
  id 'java'
}

group = 'samples'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

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)
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}

test {
  useJUnitPlatform()
}
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

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

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

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.

2.2. Select the features you need

2.2.1. Standalone

Importing ogham-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.

That’s why Ogham provides different modules so you can compose as you like:

Example 2. Jakarta Mail and FreeMarker only
Maven
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>sample</groupId>
  <artifactId>javamail-and-freemarker</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <properties>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-email-javamail</artifactId>        (1)
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-template-freemarker</artifactId>   (2)
      <version>3.0.0</version>
    </dependency>
  </dependencies>
</project>
1 Import only dependencies needed to send emails through SMTP (using Jakarta Mail).
2 Import only dependencies needed to handle FreeMarker templates.
Gradle
build.gradle
plugins {
    id 'java'
}

repositories {
  mavenLocal()
  mavenCentral()
}

dependencies {
    implementation 'fr.sii.ogham:ogham-email-javamail:3.0.0-SNAPSHOT'        (1)
    implementation 'fr.sii.ogham:ogham-template-freemarker:3.0.0-SNAPSHOT'   (2)
}
1 Import only dependencies needed to send emails through SMTP (using Jakarta Mail).
2 Import only dependencies needed to handle FreeMarker templates.

Another example for sending SMS only:

Example 3. SMPP only without using templates
Maven
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>sample</groupId>
  <artifactId>sms-only</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <properties>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-sms-cloudhopper</artifactId>    (1)
      <version>3.0.0</version>
    </dependency>
  </dependencies>
</project>
1 Import only dependencies needed to send SMS through SMPP (using Cloudhopper).
Gradle
build.gradle
plugins {
    id 'java'
}

repositories {
  mavenLocal()
  mavenCentral()
}

dependencies {
    implementation 'fr.sii.ogham:ogham-sms-cloudhopper:3.0.0-SNAPSHOT'    (1)
}
1 Import only dependencies needed to send SMS through SMPP (using Cloudhopper).

2.2.2. With Spring Boot

Ogham provides starters like Spring Boot does. If you are just interested in sending emails, just include the ogham-spring-boot-starter-email. It includes everything to work with email in Ogham plus FreeMarker and Thymeleaf but it doesn’t import anything to deal with SMS.

Example 4. Email with all features
Maven
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>samples</groupId>
  <artifactId>ogham-and-spring-boot</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>ogham-and-spring-boot-email-only</name>
  <description>Demo project for Spring Boot</description>

  <properties>
    <java.version>11</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-spring-boot-starter-email</artifactId>  (1)
      <version>3.0.0</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-freemarker</artifactId>   (2)
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>    (3)
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-mail</artifactId>         (4)
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
1 Add Ogham starter for Spring Boot to benefit for Ogham email 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.
Gradle
build.gradle
plugins {
  id 'org.springframework.boot' version '2.3.3.RELEASE'
  id 'io.spring.dependency-management' version '1.0.10.RELEASE'
  id 'java'
}

group = 'samples'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {
  implementation 'fr.sii.ogham:ogham-spring-boot-starter-email: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)
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}

test {
  useJUnitPlatform()
}
1 Add Ogham starter for Spring Boot to benefit for email 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.

If you want to be even more selective, you can manually select Ogham libraries. To benefit from Spring Boot auto-configuration, you just need to add ogham-spring-boot-autoconfigure.

Example 5. Email and FreeMarker only using Spring Boot features
Maven
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>samples</groupId>
  <artifactId>ogham-and-spring-boot</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>ogham-and-spring-boot-email-only</name>
  <description>Demo project for Spring Boot</description>

  <properties>
    <java.version>11</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-spring-boot-autoconfigure</artifactId>    (1)
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-email-javamail</artifactId>               (2)
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>fr.sii.ogham</groupId>
      <artifactId>ogham-template-freemarker</artifactId>          (3)
      <version>3.0.0</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-freemarker</artifactId>     (4)
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-mail</artifactId>           (5)
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
1 Provides auto-configuration to register Ogham beans in Spring Boot application. Also configures Ogham to use Spring Boot beans.
2 Provides Ogham features to send emails through SMTP (using Jakarta Mail).
3 Provides Ogham features to handle FreeMarker template engine.
4 Import Spring Boot FreeMarker starter as usual. Ogham will automatically adapt itself to use Spring Boot features.
5 Import Spring Boot Mail starter. Ogham will automatically adapt itself to use Spring Boot features.
Gradle
build.gradle
plugins {
  id 'org.springframework.boot' version '2.3.3.RELEASE'
  id 'io.spring.dependency-management' version '1.0.10.RELEASE'
  id 'java'
}

group = 'samples'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
  mavenCentral()
}

dependencies {
  implementation 'fr.sii.ogham:ogham-spring-boot-autoconfigure:3.0.0'   (1)
  implementation 'fr.sii.ogham:ogham-email-javamail:3.0.0'              (2)
  implementation 'fr.sii.ogham:ogham-template-freemarker:3.0.0'         (3)

  implementation 'org.springframework.boot:spring-boot-starter-freemarker'        (4)
  implementation 'org.springframework.boot:spring-boot-starter-mail'              (5)
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}

test {
  useJUnitPlatform()
}
1 Provides auto-configuration to register Ogham beans in Spring Boot application. Also configures Ogham to use Spring Boot beans.
2 Provides Ogham features to send emails through SMTP (using Jakarta Mail).
3 Provides Ogham features to handle FreeMarker template engine.
4 Import Spring Boot FreeMarker starter as usual. Ogham will automatically adapt itself to use Spring Boot features.
5 Import Spring Boot Mail starter. Ogham will automatically adapt itself to use Spring Boot features.

3. Usage

All samples with templates are using ThymeLeaf as template engine. For FreeMarker samples, take a look at FreeMarker section.

3.1. Send Email

The samples are available in the sample-standard-usage sub-project.

All samples shown bellow are using SMTP for sending email and are also valid for sending email using any other protocol/API. Only the configuration part differs.

See Sending email through SendGrid to know how to configure Ogham for sending email using SendGrid HTTP API.

3.1.1. First email using an existing SMTP server

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.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

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", "<your server host>");
    properties.put("mail.smtp.port", "<your server port>");
    properties.put("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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

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 use a configuration file.

Email address format

Ogham supports the personal information in email address. The format is personal <address>.

For example, the address with personal Ogham Test <ogham-test@yopmail.com> will result in ogham-test@yopmail.com as email address and Ogham Test as personal.

3.1.2. Use an HTML template for email body

This sample shows how to send an email with a content following a template engine language.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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
thymeleaf ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">        (1)
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title" th:text="${name}"></h1>   (2)
        <p class="text" th:text="${value}"></p>     (3)
    </body>
</html>
1 Include the ThymeLeaf namespace
2 Use the name attribute value in the template
3 Use the value attribute value in the 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 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.

3.1.3. Use HTML title as email subject

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.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlTemplateWithSubjectSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Subject is no more in Java code
thymeleaf ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Subject of the email - [[${name}]]</title>       (1)
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title" th:text="${name}"></h1>
        <p class="text" th:text="${value}"></p>
    </body>
</html>
1 The subject is defined in the template and can use same evaluation context (SimpleBean).
The subject of the email will be Subject of the email - Welcome foo !

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.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Subject is no more in Java code
freemarker logo Text template
Subject: Welcome ${name} !
Hello ${name},

Foo bar ${value}
The subject of the email will be Welcome foo !

3.1.4. HTML body with CSS and images

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:

  • <img> 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.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithImagesAndCssTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>HtmlWithImagesAndCssTemplateSample</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="css/layout.css" rel="stylesheet" />                             (1)
<link href="css/theme.css" rel="stylesheet" />                              (2)
</head>
<body>
  <header>
    <img src="images/h1.gif" alt="Creating Email Magic" />                      (3)
  </header>
  <div class="content">
    <span th:text="${name}">name</span>
    <p th:text="${value}">value</p>
    <div class="left">
      <img src="images/left.gif" data-inline-image="base64" />                  (4)
      <p class="text">
        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.
      </p>
    </div>
    <div class="right">
      <img src="images/right.gif" data-inline-image="attach" />                 (5)
      <p class="text">
        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.
      </p>
    </div>
  </div>
  <footer>
    <div class="social">
      <a href="http://www.twitter.com/">
        <img src="images/tw.gif" alt="Twitter" />                       (6)
      </a>
      <a href="http://www.facebook.com/">
        <img src="images/fb.gif" alt="Facebook" data-inline-image="skip" />           (7)
      </a>
    </div>
    <div class="sender-info">
      &reg; Someone, somewhere 2013<br />
      <a href="#" class="white" data-inline-styles="skip">Unsubscribe</a> to this newsletter instantly  (8)
    </div>
  </footer>
</body>
</html>
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 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 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.
css CSS
layout.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
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;
}
html Sent HTML
  • Add code of the generated HTML with all transformations

Here is the list of supported transformations:

  • <img> tags that use local images are embedded (using cid reference)

  • <img> tags that use local images are embedded (using base64 data URI)

  • Inline CSS rules using style attribute

  • background images that use local images are embedded (using cid reference)

  • 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

  • List all other rules

  • Indicate what will be supported and what won’t and why

3.1.4.1. Inline images

Images can be referenced externally but there are many constraints to do so. You have to provide the URL of the image. However, while you are coding or testing the feature that sends an email with images (for example testing account creation with confirmation email), the URL must point to an existing and accessible image. And even then, the email client might block those images for safety purpose (prevent tracking). Not mentioning offline issues.

That’s why Ogham provides automatic inlining on local images. Therefore, no need to use a server to serve the images and no need to configure the URLs to point to those images. The images are:

  • Either attached with the email and referenced in the HTML using a Content-ID.

  • Or images are converted to a base64 string and embedded in the HTML using the data URL scheme

  • Or not inlined at all (if the image points to an external URL or it is explicitly skipped)

Sender implementation independent

Inlining of images not only work when sending an email using Java Mail but also works for any other email sender (like SendGrid).

Attach and reference images

By default, if the HTML contains <img> tag with src attribute that points to a local image, the image is automatically attached and referenced using a Content-ID that is automatically generated.

Default choice explanation

The attach strategy has been chosen by default as it is handled by more email clients than base64 strategy.

Customize CID generation

Default Content-ID generation simply uses a incremental number. It is possible to use custom Content-ID generation.

As Ogham is fully configurable, the default strategy can be changed. Therefore if you want to use this strategy for a particular image, you need to explicitly indicate it. You can do so by setting the data-inline-image attribute with the "attach" value directly on the <img> tag.

Change default strategy

It is possible to configure Ogham to use a different inlining strategy by default.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithImagesAndCssTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>HtmlWithImagesAndCssTemplateSample</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="css/layout.css" rel="stylesheet" />                             (1)
<link href="css/theme.css" rel="stylesheet" />                              (2)
</head>
<body>
  <header>
    <img src="images/h1.gif" alt="Creating Email Magic" />                      (3)
  </header>
  <div class="content">
    <span th:text="${name}">name</span>
    <p th:text="${value}">value</p>
    <div class="left">
      <img src="images/left.gif" data-inline-image="base64" />                  (4)
      <p class="text">
        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.
      </p>
    </div>
    <div class="right">
      <img src="images/right.gif" data-inline-image="attach" />                 (5)
      <p class="text">
        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.
      </p>
    </div>
  </div>
  <footer>
    <div class="social">
      <a href="http://www.twitter.com/">
        <img src="images/tw.gif" alt="Twitter" />                       (6)
      </a>
      <a href="http://www.facebook.com/">
        <img src="images/fb.gif" alt="Facebook" data-inline-image="skip" />           (7)
      </a>
    </div>
    <div class="sender-info">
      &reg; Someone, somewhere 2013<br />
      <a href="#" class="white" data-inline-styles="skip">Unsubscribe</a> to this newsletter instantly  (8)
    </div>
  </footer>
</body>
</html>
1 _
2 _
3 As nothing is specified and src points to a local image, the image will be inlined (using attach strategy)
4 _
5 Same as <3> but strategy is explicitly indicated
6 Same as <3>
html Generated HTML
<!doctype html>
<html>
<head>                                                (1)
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Demystifying Email Design</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 10px auto 30px auto; width: 600px; border: 1px solid #ccc;">         (2)
  <header style="padding: 40px 30px; background: #70bbd9;">
    <img src="cid:0" alt="Creating Email Magic" style="display: block; margin: auto;">      (3)
  </header>
  <div class="content" style="padding: 40px 30px; text-align: justify;">              (4)
    <span>foo</span>
    <p>42</p>
    <div class="left" style="width: 250px; display: inline-block; margin-right: 36px;">
      <img src="data:image/gif;base64,R0lGODlh+gCMANUAAOxRVfySUllNTaNvTPS1j
            [...]
            K1KIa9ahITapSl8rUpjr1qVCNqlSHFAQAOw==">                   (5)
      <p class="text">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.
      </p>
    </div>
    <div class="right" style="width: 250px; display: inline-block;">
      <img src="cid:1">                                   (6)
      <p class="text">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.
      </p>
    </div>
  </div>
  <footer style="padding: 30px; color: #ffffff; font-family: Arial, sans-serif; font-size: 14px; background: #ee4c50;">
    <div class="social" style="text-align: center;">
      <a href="http://www.twitter.com/" style="display: inline-block; color: #ffffff;">   (7)
        <img src="cid:2" alt="Twitter">                           (8)
      </a>
      <a href="http://www.facebook.com/" style="display: inline-block; color: #ffffff;">    (9)
        <img src="images/fb.gif" alt="Facebook">                      (10)
      </a>
    </div>
    <div class="sender-info" style="text-align: center; margin-top: 20px;">
      &reg; Someone, somewhere 2013 <br>
      <a href="#" class="white">Unsubscribe</a>to this newsletter instantly         (11)
    </div>
  </footer>
</body>
</html>
1 _
2 _
3 The image is attached and the generated Content-ID is 0.
4 _
5 _
6 The image is attached and the generated Content-ID is 1.
7 _
8 The image is attached and the generated Content-ID is 2.
Embed using data URL scheme

As stated before, by default images are attached. So if you want to embed an image directly as a base64 string, you need to set the data-inline-image attribute with the "base64" value directly on the <img> tag.

Mimetype detection

The data URL scheme needs the mediatype information. Ogham automatically provides this information for you thanks to automatic mimetype detection.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithImagesAndCssTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>HtmlWithImagesAndCssTemplateSample</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="css/layout.css" rel="stylesheet" />                             (1)
<link href="css/theme.css" rel="stylesheet" />                              (2)
</head>
<body>
  <header>
    <img src="images/h1.gif" alt="Creating Email Magic" />                      (3)
  </header>
  <div class="content">
    <span th:text="${name}">name</span>
    <p th:text="${value}">value</p>
    <div class="left">
      <img src="images/left.gif" data-inline-image="base64" />                  (4)
      <p class="text">
        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.
      </p>
    </div>
    <div class="right">
      <img src="images/right.gif" data-inline-image="attach" />                 (5)
      <p class="text">
        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.
      </p>
    </div>
  </div>
  <footer>
    <div class="social">
      <a href="http://www.twitter.com/">
        <img src="images/tw.gif" alt="Twitter" />                       (6)
      </a>
      <a href="http://www.facebook.com/">
        <img src="images/fb.gif" alt="Facebook" data-inline-image="skip" />           (7)
      </a>
    </div>
    <div class="sender-info">
      &reg; Someone, somewhere 2013<br />
      <a href="#" class="white" data-inline-styles="skip">Unsubscribe</a> to this newsletter instantly  (8)
    </div>
  </footer>
</body>
</html>
1 _
2 _
3 _
4 Indicate to Ogham to inline using base64 strategy
html Generated HTML
<!doctype html>
<html>
<head>                                                (1)
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Demystifying Email Design</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 10px auto 30px auto; width: 600px; border: 1px solid #ccc;">         (2)
  <header style="padding: 40px 30px; background: #70bbd9;">
    <img src="cid:0" alt="Creating Email Magic" style="display: block; margin: auto;">      (3)
  </header>
  <div class="content" style="padding: 40px 30px; text-align: justify;">              (4)
    <span>foo</span>
    <p>42</p>
    <div class="left" style="width: 250px; display: inline-block; margin-right: 36px;">
      <img src="data:image/gif;base64,R0lGODlh+gCMANUAAOxRVfySUllNTaNvTPS1j
            [...]
            K1KIa9ahITapSl8rUpjr1qVCNqlSHFAQAOw==">                   (5)
      <p class="text">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.
      </p>
    </div>
    <div class="right" style="width: 250px; display: inline-block;">
      <img src="cid:1">                                   (6)
      <p class="text">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.
      </p>
    </div>
  </div>
  <footer style="padding: 30px; color: #ffffff; font-family: Arial, sans-serif; font-size: 14px; background: #ee4c50;">
    <div class="social" style="text-align: center;">
      <a href="http://www.twitter.com/" style="display: inline-block; color: #ffffff;">   (7)
        <img src="cid:2" alt="Twitter">                           (8)
      </a>
      <a href="http://www.facebook.com/" style="display: inline-block; color: #ffffff;">    (9)
        <img src="images/fb.gif" alt="Facebook">                      (10)
      </a>
    </div>
    <div class="sender-info" style="text-align: center; margin-top: 20px;">
      &reg; Someone, somewhere 2013 <br>
      <a href="#" class="white">Unsubscribe</a>to this newsletter instantly         (11)
    </div>
  </footer>
</body>
</html>
1 _
2 _
3 _
4 _
5 The data URL starts with data: followed by the mimetype (image/gif) and the image converted to a base64 string (the base64 string is partially shown here for readability).
Skip some images

In the case you need to inline an image manually, you can tell Ogham to skip the image by placing the data-inline-image attribute with the "skip" value on the <img> tag.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithImagesAndCssTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>HtmlWithImagesAndCssTemplateSample</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="css/layout.css" rel="stylesheet" />                             (1)
<link href="css/theme.css" rel="stylesheet" />                              (2)
</head>
<body>
  <header>
    <img src="images/h1.gif" alt="Creating Email Magic" />                      (3)
  </header>
  <div class="content">
    <span th:text="${name}">name</span>
    <p th:text="${value}">value</p>
    <div class="left">
      <img src="images/left.gif" data-inline-image="base64" />                  (4)
      <p class="text">
        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.
      </p>
    </div>
    <div class="right">
      <img src="images/right.gif" data-inline-image="attach" />                 (5)
      <p class="text">
        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.
      </p>
    </div>
  </div>
  <footer>
    <div class="social">
      <a href="http://www.twitter.com/">
        <img src="images/tw.gif" alt="Twitter" />                       (6)
      </a>
      <a href="http://www.facebook.com/">
        <img src="images/fb.gif" alt="Facebook" data-inline-image="skip" />           (7)
      </a>
    </div>
    <div class="sender-info">
      &reg; Someone, somewhere 2013<br />
      <a href="#" class="white" data-inline-styles="skip">Unsubscribe</a> to this newsletter instantly  (8)
    </div>
  </footer>
</body>
</html>
1 _
2 _
3 _
4 _
5 _
6 _
7 Indicate to Ogham to skip inlining using skip value
html Generated HTML
<!doctype html>
<html>
<head>                                                (1)
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Demystifying Email Design</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 10px auto 30px auto; width: 600px; border: 1px solid #ccc;">         (2)
  <header style="padding: 40px 30px; background: #70bbd9;">
    <img src="cid:0" alt="Creating Email Magic" style="display: block; margin: auto;">      (3)
  </header>
  <div class="content" style="padding: 40px 30px; text-align: justify;">              (4)
    <span>foo</span>
    <p>42</p>
    <div class="left" style="width: 250px; display: inline-block; margin-right: 36px;">
      <img src="data:image/gif;base64,R0lGODlh+gCMANUAAOxRVfySUllNTaNvTPS1j
            [...]
            K1KIa9ahITapSl8rUpjr1qVCNqlSHFAQAOw==">                   (5)
      <p class="text">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.
      </p>
    </div>
    <div class="right" style="width: 250px; display: inline-block;">
      <img src="cid:1">                                   (6)
      <p class="text">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.
      </p>
    </div>
  </div>
  <footer style="padding: 30px; color: #ffffff; font-family: Arial, sans-serif; font-size: 14px; background: #ee4c50;">
    <div class="social" style="text-align: center;">
      <a href="http://www.twitter.com/" style="display: inline-block; color: #ffffff;">   (7)
        <img src="cid:2" alt="Twitter">                           (8)
      </a>
      <a href="http://www.facebook.com/" style="display: inline-block; color: #ffffff;">    (9)
        <img src="images/fb.gif" alt="Facebook">                      (10)
      </a>
    </div>
    <div class="sender-info" style="text-align: center; margin-top: 20px;">
      &reg; Someone, somewhere 2013 <br>
      <a href="#" class="white">Unsubscribe</a>to this newsletter instantly         (11)
    </div>
  </footer>
</body>
</html>
1 _
2 _
3 _
4 _
5 _
6 _
7 _
8 _
9 _
10 The image is not inlined at all (the original src attribute is unchanged)
Future thoughts

In future version of Ogham, a new strategy may be added in order to automatically serve local images and fill the HTML with the associated URL.

The images may be automatically served by:

  • first uploading them somewhere before sending the email

  • automatically registering endpoints into your web application (adding Spring controller for example) that uses Ogham

  • starting a local server (by running a command or a docker image for example)

  • using tools like ngrok to expose local files/http server

  • …​

3.1.4.2. Inline CSS

Email clients doesn’t handle external CSS files at all. Styles can be included in a style tag but some email clients like Gmail doesn’t support it. So all rules provided in the CSS MUST be inlined directly in the HTML. Writing code like this is really bad practice:

  • code is difficult to read

  • it is error prone as updating for example the base color need to ensure that styles are updated everywhere

  • code can’t be mutualized between several HTML files

  • …​

HTML files are then hard to maintain.

That’s why Ogham will automatically inline CSS rules directly on the HTML.

Sender implementation independent

CSS inlining not only work when sending an email using Java Mail but also works for any other email sender (like SendGrid).

Inline styles on HTML nodes

By default, the CSS rules contained in CSS files referenced in the HTML are inlined. Each node has their style attribute updated to add the CSS rules that match that node.

Default choice explanation

Most of email clients only support theming using style attribute. Even modern email clients doesn’t support other strategies such as using <style> tags.

Inlining of <style> nodes

As <style> may not be supported, the styles contained in <style> nodes in the HTML are also inlined using style attribute.

  • When Ogham provides several CSS inlining modes, add information about how to change default strategy

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithImagesAndCssTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>HtmlWithImagesAndCssTemplateSample</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="css/layout.css" rel="stylesheet" />                             (1)
<link href="css/theme.css" rel="stylesheet" />                              (2)
</head>
<body>
  <header>
    <img src="images/h1.gif" alt="Creating Email Magic" />                      (3)
  </header>
  <div class="content">
    <span th:text="${name}">name</span>
    <p th:text="${value}">value</p>
    <div class="left">
      <img src="images/left.gif" data-inline-image="base64" />                  (4)
      <p class="text">
        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.
      </p>
    </div>
    <div class="right">
      <img src="images/right.gif" data-inline-image="attach" />                 (5)
      <p class="text">
        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.
      </p>
    </div>
  </div>
  <footer>
    <div class="social">
      <a href="http://www.twitter.com/">
        <img src="images/tw.gif" alt="Twitter" />                       (6)
      </a>
      <a href="http://www.facebook.com/">
        <img src="images/fb.gif" alt="Facebook" data-inline-image="skip" />           (7)
      </a>
    </div>
    <div class="sender-info">
      &reg; Someone, somewhere 2013<br />
      <a href="#" class="white" data-inline-styles="skip">Unsubscribe</a> to this newsletter instantly  (8)
    </div>
  </footer>
</body>
</html>
1 Reference a CSS file that contains general layout rules
2 Reference a CSS file that contains theming (like colors) rules
css CSS files
layout.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
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;
}
html Generated HTML
<!doctype html>
<html>
<head>                                                (1)
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Demystifying Email Design</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 10px auto 30px auto; width: 600px; border: 1px solid #ccc;">         (2)
  <header style="padding: 40px 30px; background: #70bbd9;">
    <img src="cid:0" alt="Creating Email Magic" style="display: block; margin: auto;">      (3)
  </header>
  <div class="content" style="padding: 40px 30px; text-align: justify;">              (4)
    <span>foo</span>
    <p>42</p>
    <div class="left" style="width: 250px; display: inline-block; margin-right: 36px;">
      <img src="data:image/gif;base64,R0lGODlh+gCMANUAAOxRVfySUllNTaNvTPS1j
            [...]
            K1KIa9ahITapSl8rUpjr1qVCNqlSHFAQAOw==">                   (5)
      <p class="text">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.
      </p>
    </div>
    <div class="right" style="width: 250px; display: inline-block;">
      <img src="cid:1">                                   (6)
      <p class="text">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.
      </p>
    </div>
  </div>
  <footer style="padding: 30px; color: #ffffff; font-family: Arial, sans-serif; font-size: 14px; background: #ee4c50;">
    <div class="social" style="text-align: center;">
      <a href="http://www.twitter.com/" style="display: inline-block; color: #ffffff;">   (7)
        <img src="cid:2" alt="Twitter">                           (8)
      </a>
      <a href="http://www.facebook.com/" style="display: inline-block; color: #ffffff;">    (9)
        <img src="images/fb.gif" alt="Facebook">                      (10)
      </a>
    </div>
    <div class="sender-info" style="text-align: center; margin-top: 20px;">
      &reg; Someone, somewhere 2013 <br>
      <a href="#" class="white">Unsubscribe</a>to this newsletter instantly         (11)
    </div>
  </footer>
</body>
</html>
1 The <style> tags are removed from the head
2 Apply body rules using style attribute. The rules from both files are merged.
3 _
4 Apply .content rules using style attribute. The rules from both files are merged.
5 _
6 _
7 Apply footer .social a rules using style attribute. The rules from both files are merged.
8 _
9 As footer .social a also matches this node, the same rules as <7> are applied.
Skip some styles

If you don’t want to apply styles on a particular node, you can set the attribute data-inline-styles to "skip" value.

Skip some rules

If you set data-inline-styles attribute to "skip" value on <link> or <style> nodes, the whole contained rules are not applied at all.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithImagesAndCssTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>HtmlWithImagesAndCssTemplateSample</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="css/layout.css" rel="stylesheet" />                             (1)
<link href="css/theme.css" rel="stylesheet" />                              (2)
</head>
<body>
  <header>
    <img src="images/h1.gif" alt="Creating Email Magic" />                      (3)
  </header>
  <div class="content">
    <span th:text="${name}">name</span>
    <p th:text="${value}">value</p>
    <div class="left">
      <img src="images/left.gif" data-inline-image="base64" />                  (4)
      <p class="text">
        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.
      </p>
    </div>
    <div class="right">
      <img src="images/right.gif" data-inline-image="attach" />                 (5)
      <p class="text">
        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.
      </p>
    </div>
  </div>
  <footer>
    <div class="social">
      <a href="http://www.twitter.com/">
        <img src="images/tw.gif" alt="Twitter" />                       (6)
      </a>
      <a href="http://www.facebook.com/">
        <img src="images/fb.gif" alt="Facebook" data-inline-image="skip" />           (7)
      </a>
    </div>
    <div class="sender-info">
      &reg; Someone, somewhere 2013<br />
      <a href="#" class="white" data-inline-styles="skip">Unsubscribe</a> to this newsletter instantly  (8)
    </div>
  </footer>
</body>
</html>
1 Reference a CSS file that contains general layout rules
2 Reference a CSS file that contains theming (like colors) rules
3 _
4 _
5 _
6 _
7 _
8 Indicate Ogham to not apply styles on this node
css CSS files
layout.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
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;
}
html Generated HTML
<!doctype html>
<html>
<head>                                                (1)
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Demystifying Email Design</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 10px auto 30px auto; width: 600px; border: 1px solid #ccc;">         (2)
  <header style="padding: 40px 30px; background: #70bbd9;">
    <img src="cid:0" alt="Creating Email Magic" style="display: block; margin: auto;">      (3)
  </header>
  <div class="content" style="padding: 40px 30px; text-align: justify;">              (4)
    <span>foo</span>
    <p>42</p>
    <div class="left" style="width: 250px; display: inline-block; margin-right: 36px;">
      <img src="data:image/gif;base64,R0lGODlh+gCMANUAAOxRVfySUllNTaNvTPS1j
            [...]
            K1KIa9ahITapSl8rUpjr1qVCNqlSHFAQAOw==">                   (5)
      <p class="text">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.
      </p>
    </div>
    <div class="right" style="width: 250px; display: inline-block;">
      <img src="cid:1">                                   (6)
      <p class="text">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.
      </p>
    </div>
  </div>
  <footer style="padding: 30px; color: #ffffff; font-family: Arial, sans-serif; font-size: 14px; background: #ee4c50;">
    <div class="social" style="text-align: center;">
      <a href="http://www.twitter.com/" style="display: inline-block; color: #ffffff;">   (7)
        <img src="cid:2" alt="Twitter">                           (8)
      </a>
      <a href="http://www.facebook.com/" style="display: inline-block; color: #ffffff;">    (9)
        <img src="images/fb.gif" alt="Facebook">                      (10)
      </a>
    </div>
    <div class="sender-info" style="text-align: center; margin-top: 20px;">
      &reg; Someone, somewhere 2013 <br>
      <a href="#" class="white">Unsubscribe</a>to this newsletter instantly         (11)
    </div>
  </footer>
</body>
</html>
1 _
2 _
3 _
4 _
5 _
6 _
7 _
8 _
9 _
10 _
11 .white rule defined in theming.css matches this node however, you can notice that no style attribute has been added
Future thoughts

In future version of Ogham, a new strategy may be added in order to include styles using <style> tags.

3.1.4.3. Inline background images

As developer you may need to use background images to make your email more attractive. As for <img> tags, the images have to be either served from a server that is available online (and referenced by an URL) or embedded with the email.

Ogham provides automatic inlining of images included through CSS. It works for the following properties:

  • background, background-image

  • list-style, list-style-image

  • cursor

Email client support

Even if the properties background, background-image, list-style, list-style-image and cursor can be inlined by Ogham, it doesn’t mean that email clients support those properties.

Use a background color as fallback

We recommend that you provide a background color in addition to a background image as fallback in case that the image is not displayed by the email client.

As well as <img> inlining, Ogham provides the following strategies:

  • Either attached with the email and referenced in the HTML using a Content-ID.

  • Or images are converted to a base64 string and embedded in the HTML using the data URL scheme

  • Or not inlined at all (if the image points to an external URL or it is explicitly skipped)

Automatically skip external URLs

If the url() value references an external image (starting with http:// or https://), the image is not inlined at all.

Images referenced in the HTML

In addition to CSS rules defined in external CSS file, Ogham also supports inlining of images referenced either in <style> tags or directly on HTML tags using style attribute.

Attach and reference images

By default, if a CSS rule contains one of background, background-image, list-style, list-style-image or cursor properties with an url() that points to a local image, the image is automatically attached and referenced using a Content-ID that is automatically generated.

Default choice explanation

Like <img> inlining, attach strategy is handled by more clients than base64 strategy.

Customize CID generation

Default Content-ID generation simply uses a incremental number. It is possible to use custom Content-ID generation.

Instead of using default strategy, it is possible to explicitly indicate which inlining strategy to use for a particular image. This is done through --inline-image property.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithBackgroundImagesTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 (if provided)
    service.send(new Email()
            .body().template("classpath:/template/thymeleaf/background-images",    (1)
                    new SimpleBean("foo", 42))                             (2)
            .to("ogham-test@yopmail.com"));
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 The path to the templates (/template/thymeleaf/background-images.html for the main body
2 The template context
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Inline background images</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="/resources/css/background-images.css" rel="stylesheet" />       (1)
</head>
<body>
  <div class="header">                                                    (2)
    Hello [[${name}]] !
  </div>
  <div class="content">                                                   (3)
    content
  </div>
  <div class="footer">                                                    (4)
    footer
  </div>
</body>
</html>
1 Include CSS file that contains rules with background images
2 A node that is targeted by a CSS rule that uses a background image. The background image will be inlined (using attach strategy).
3 _
4 _
css CSS
.header {
  background: #ccc url("../images/header.jpg") repeat-x;    /* (1)
  */
  height: 200px;
}

.footer {
  background: url("../images/footer.jpg") no-repeat;        /* (2)
  */
  height: 200px;
  --inline-image: base64;                                   /* (3)
  */
}

.content {
  background: url("../images/h1.gif") no-repeat;            /* (4)
  */
  --inline-image: skip;                                     /* (5)
  */
  height: 400px;
}
1 A background property that references a local image using url(). As nothing is specified, the image will be inlined using attach strategy.
2 _
3 _
4 _
5 _
html Generated HTML
<!doctype html>
<html>
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Inline background images</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
                                                                                                                                                             (1)
 </head>
 <body>
  <div class="header" style="background: #ccc url(&quot;cid:0&quot;) repeat-x;  height: 200px;">                                                             (2)
   Hello foo !
  </div>
  <div class="content" style="background: url(&quot;classpath:/resources/images/h1.gif&quot;) no-repeat;  height: 400px;  ">                               (3)
   content
  </div>
  <div class="footer" style="background: url(&quot;data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkq[...]JpRFcBO8BIP/2Q==&quot;) no-repeat; height: 200px;  ">   (4)
   footer
  </div>
 </body>
</html>
1 The CSS file is inlined (so the inclusion is removed)
2 The image is attached and the generated Content-ID is 0.
3 _
4 _
Embed using data URL scheme

Instead of attaching the image with the email, it is possible to inline images included through CSS rules directly as a base64 string. To enable it, you need to provide the --inline-image property with base64 value.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithBackgroundImagesTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 (if provided)
    service.send(new Email()
            .body().template("classpath:/template/thymeleaf/background-images",    (1)
                    new SimpleBean("foo", 42))                             (2)
            .to("ogham-test@yopmail.com"));
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 The path to the templates (/template/thymeleaf/background-images.html for the main body
2 The template context
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Inline background images</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="/resources/css/background-images.css" rel="stylesheet" />       (1)
</head>
<body>
  <div class="header">                                                    (2)
    Hello [[${name}]] !
  </div>
  <div class="content">                                                   (3)
    content
  </div>
  <div class="footer">                                                    (4)
    footer
  </div>
</body>
</html>
1 Include CSS file that contains rules with background images
2 _
3 _
4 A node that is targeted by a CSS rule that uses a background image. The background image will be inlined (using base64 strategy).
css CSS
.header {
  background: #ccc url("../images/header.jpg") repeat-x;    /* (1)
  */
  height: 200px;
}

.footer {
  background: url("../images/footer.jpg") no-repeat;        /* (2)
  */
  height: 200px;
  --inline-image: base64;                                   /* (3)
  */
}

.content {
  background: url("../images/h1.gif") no-repeat;            /* (4)
  */
  --inline-image: skip;                                     /* (5)
  */
  height: 400px;
}
1 _
2 A background property that references a local image using url(). The inline strategy is specified using --inline-image property on the same CSS rule.
3 Indicate the inlining strategy: use base64 for all images contained in .footer rule.
4 _
5 _
html Generated HTML
<!doctype html>
<html>
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Inline background images</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
                                                                                                                                                             (1)
 </head>
 <body>
  <div class="header" style="background: #ccc url(&quot;cid:0&quot;) repeat-x;  height: 200px;">                                                             (2)
   Hello foo !
  </div>
  <div class="content" style="background: url(&quot;classpath:/resources/images/h1.gif&quot;) no-repeat;  height: 400px;  ">                               (3)
   content
  </div>
  <div class="footer" style="background: url(&quot;data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkq[...]JpRFcBO8BIP/2Q==&quot;) no-repeat; height: 200px;  ">   (4)
   footer
  </div>
 </body>
</html>
1 _
2 _
3 _
4 The style attribute contains the background property with the image content converted as a base64 string (the base64 string is partially shown here for readability).
Use different strategies for images defined in the same CSS rule

It is possible to reference several images (either background property with multiple background images or several properties) on the same CSS rule. --inline-image allows a more advanced syntax that allows to provide a strategy for a specific image.

Skip some images
java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlWithBackgroundImagesTemplateSample {
  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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 (if provided)
    service.send(new Email()
            .body().template("classpath:/template/thymeleaf/background-images",    (1)
                    new SimpleBean("foo", 42))                             (2)
            .to("ogham-test@yopmail.com"));
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 The path to the templates (/template/thymeleaf/background-images.html for the main body
2 The template context
thymeleaf html ThymeLeaf template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Inline background images</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="/resources/css/background-images.css" rel="stylesheet" />       (1)
</head>
<body>
  <div class="header">                                                    (2)
    Hello [[${name}]] !
  </div>
  <div class="content">                                                   (3)
    content
  </div>
  <div class="footer">                                                    (4)
    footer
  </div>
</body>
</html>
1 Include CSS file that contains rules with background images
2 _
3 A node that is targeted by a CSS rule that uses a background image. The background image will not be inlined.
4 _
css CSS
.header {
  background: #ccc url("../images/header.jpg") repeat-x;    /* (1)
  */
  height: 200px;
}

.footer {
  background: url("../images/footer.jpg") no-repeat;        /* (2)
  */
  height: 200px;
  --inline-image: base64;                                   /* (3)
  */
}

.content {
  background: url("../images/h1.gif") no-repeat;            /* (4)
  */
  --inline-image: skip;                                     /* (5)
  */
  height: 400px;
}
1 _
2 _
3 _
4 A background property that references a local image using url(). The inline strategy is specified using --inline-image property on the same CSS rule.
5 Indicate the inlining strategy (skip): do not inline any image contained in .content rule.
html Generated HTML
<!doctype html>
<html>
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Inline background images</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
                                                                                                                                                             (1)
 </head>
 <body>
  <div class="header" style="background: #ccc url(&quot;cid:0&quot;) repeat-x;  height: 200px;">                                                             (2)
   Hello foo !
  </div>
  <div class="content" style="background: url(&quot;classpath:/resources/images/h1.gif&quot;) no-repeat;  height: 400px;  ">                               (3)
   content
  </div>
  <div class="footer" style="background: url(&quot;data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkq[...]JpRFcBO8BIP/2Q==&quot;) no-repeat; height: 200px;  ">   (4)
   footer
  </div>
 </body>
</html>
1 _
2 _
3 The style attribute contains the background property with unchanged url to the image (except for the path, see note below).
4 _
Relative resources

It is possible to reference images from CSS using relative path. When Ogham inlines the CSS rules in the HTML using style attribute, the path to local images relative to the CSS file are updated to the absolute path. This way, the path in the HTML points to the right file.

Indicate a different strategy for specific images

background and background-image properties allow to use multiple backgrounds. Moreover, you could use several properties that reference different images on the same CSS rule. That’s why Ogham provides a more advanced syntax to be able to specify which strategy to use for which image.

The possible syntaxes are:

  • --inline-image: <strategy>; to apply the same stategy to all images that need to be inlined.

  • --inline-image: <image1>=<strategy1> <image2>=<strategy2> …​ <imageN>=<strategyN>; to apply <strategy1> on image that matches the path defined by <image1>, <strategy2> on image that matches the path defined by <image2>, and so on.

  • --inline-image: <image1>=<strategy1> <image2>=<strategy2> …​ <imageN>=<strategyN> <default strategy>;: same as before but for all images that don’t match any path, it applies the <default strategy>.

Example 6. Same strategy for all images
css CSS
.same-strategy-for-all-images {
  background-image:
    no-repeat url('/static/images/h1.gif') top center,     (1)
    url('../images/right.gif');                            (2)
  list-style-image: url('../images/disc.png');               (3)
  --inline-image: base64;                                    (4)
}
1 A first background that references the image h1.gif available in classpath subfolder static/images.
2 A second background that references the local image right.gif that is relative to the CSS file.
3 Use the local image disc.png to style the list bullets.
4 Indicate the inlining strategy (base64) for the three images.
Example 7. Strategy per image
css CSS
.strategy-per-images {
  background-image:
    no-repeat url('/static/images/h1.gif') top center,              (1)
    url('../images/right.gif');                                     (2)
  list-style-image: url('../images/disc.png');                        (3)
  --inline-image: h1=base64 images/right.gif=attach disc.png=skip;    (4)
}
1 A first background that references the image h1.gif available in classpath folder static/images.
2 A second background that references the local image right.gif that is relative to the CSS file.
3 Use the local image disc.png to style the list bullets.
4 Indicate the inlining strategy per image:
  • Use base64 strategy for image h1.gif

  • Use attach strategy for image right.gif

  • Skip inlining for image disc.png

Image matching

The image matching is simply a contains test. It means that if you write --inline-image: gif=base64 png=attach;, base64 strategy is applied on h1.gif and right.gif while attach strategy is used for disc.png in the example above.

Example 8. Strategy per image with default
css CSS
.strategy-per-images-with-default {
  background-image:
    no-repeat url('/static/images/h1.gif') top center,              (1)
    url('../images/right.gif');                                     (2)
  list-style-image: url('../images/disc.png');                        (3)
  --inline-image: h1=base64 attach;                                   (4)
}
1 A first background that references the image h1.gif available in classpath folder static/images.
2 A second background that references the local image right.gif that is relative to the CSS file.
3 Use the local image disc.png to style the list bullets.
4 Indicate the inlining strategies:
  • Use base64 strategy for image h1.gif

  • Use attach strategy for all other images defined in the rule (right.gif and disc.png)

3.1.5. A working preview of the HTML body (text alternative)

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.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class HtmlAndTextSample {
  private static String html = "<!DOCTYPE html>"
                + "<html>"
                +   "<head><meta charset=\"utf-8\" /></head>"
                +   "<body>"
                +     "<h1 class=\"title\">Hello World</h1>"
                +     "<p class=\"text\">Foo bar</p>"
                +   "</body>"
                + "</html>";
  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", "<your server host>");
    properties.put("mail.smtp.port", "<your server port>");
    properties.put("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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)

The call order between text() and html() doesn’t matter (unlike using .content(new MultiContent(…​))).

The underlying Content is a MultiContent.

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).

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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)

The underlying Content is a MultiTemplateContent.

thymeleaf text Text template
Text template located in src/main/resources/template/thymeleaf/simple.txt
[[${name}]] [[${value}]]
thymeleaf html HTML template
HTML template located at src/main/resources/template/thymeleaf/simple.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">        (1)
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title" th:text="${name}"></h1>   (2)
        <p class="text" th:text="${value}"></p>     (3)
    </body>
</html>

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.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

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", "<your server host>");
    properties.setProperty("mail.smtp.port", "<your server port>");
    properties.setProperty("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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)
freemarker logo Text template
Text template located in src/main/resources/template/mixed/simple.txt.ftl
${name} ${value}
thymeleaf HTML template
HTML template located at src/main/resources/template/mixed/simple.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title" th:text="${name}"></h1>
        <p class="text" th:text="${value}"></p>
    </body>
</html>

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.

3.1.6. Attach files to the email

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class WithAttachmentSample {
  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", "<your server host>");
    properties.put("mail.smtp.port", "<your server port>");
    properties.put("ogham.email.from.default-value", "<email address to display for the sender user>");
    // 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

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 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.

If you are using InputStream, you need to close the stream after sending the email.
You can also add a custom description for any attachment but in this case use Attachment class.

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 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.

3.1.7. Globally configure default email fields

3.1.7.1. Globally configure default sender

You can configure sender address for all sent email by setting the property ogham.email.from.default-value. The value can either be an email address (user@domain.host) or an address with personal information (User Name <user@domain.host>). This property is used for every implementation (through SMTP, through SendGrid, …​).

This global address is used only if nothing is specified in the email. If you explicitly set the sender address in the email constructor or using the setter, this value is used instead of the global one.

java logo Java
package fr.sii.ogham.sample.standard.email;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class OverrideDefaultSenderSample {
  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", "<your server host>");
    properties.put("mail.smtp.port", "<your server port>");
    properties.put("ogham.email.from.default-value", "foo.bar@test.com");  (1)
    // 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()                                               (2)
            .subject("OverrideDefaultSenderSample")
            .body().string("email content")
            .to("ogham-test@yopmail.com"));
    // => the sender address is foo.bar@test.com

    service.send(new Email()
        .subject("subject")
        .body().string("email content")
        .from("override@test.com")                                     (3)
        .to("ogham-test@yopmail.com"));
    // => the sender address is now override@test.com
  }
}
1 Set the default sender address globally using properties
2 Do not provide from field so the sender address is foo.bar@test.com
3 Override the default sender address by providing a from field. The address is now override@test.com
mail.from and mail.smtp.from also work
3.1.7.2. Globally configure default subject

As for sender address, you can define globally a default subject for emails if none is explicitly provided (neither using .subject(String) method nor defining a subject directly in the template). The property is ogham.email.subject.default-value.

3.1.7.3. Globally configure default recipients

You can also use properties to define default recipients if none are provided:

  • ogham.email.to.default-value: set one or several recipient addresses (to field)

  • ogham.email.cc.default-value: set one or several recipient addresses (cc field)

  • ogham.email.bcc.default-value: set one or several recipient addresses (bcc field)

This can be convenient to set a bcc address for all sent messages for example (the bcc address will never be seen in received emails).

To define several recipient addresses, you can provide a string separated by ,.

java logo Java
package oghamall.it.email;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;
import java.util.Properties;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.testing.extension.junit.LoggingTestRule;
import fr.sii.ogham.testing.extension.junit.email.RandomPortGreenMailRule;

public class EmailPropertiesTest {

  private MessagingService oghamService;

  @Rule
  public final LoggingTestRule loggingRule = new LoggingTestRule();

  @Rule
  public final GreenMailRule greenMail = new RandomPortGreenMailRule();

  @Before
  public void setUp() throws IOException {
    Properties additional = new Properties();
    additional.setProperty("mail.smtp.host", greenMail.getSmtp().getBindTo());
    additional.setProperty("mail.smtp.port", String.valueOf(greenMail.getSmtp().getPort()));
    additional.setProperty("ogham.email.from.default-value", "test.sender@sii.fr");
    additional.setProperty("ogham.email.to.default-value", "recipient.to1@sii.fr,  recipient.to2@sii.fr , recipient.to3@sii.fr");   (1)
    additional.setProperty("ogham.email.cc.default-value", "recipient.cc1@sii.fr,recipient.cc2@sii.fr");                            (2)
    additional.setProperty("ogham.email.bcc.default-value", "recipient.bcc@sii.fr");                                                (3)
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties("/application.properties")
          .properties(additional)
          .and()
        .build();
  }

  @Test
  public void simple() throws MessagingException, javax.mail.MessagingException {
    oghamService.send(new Email()
              .subject("Simple")
              .content("string body"));
    assertThat(greenMail).receivedMessages()
        .count(is(6))
        .every()
          .subject(is("Simple"))
          .body()
            .contentAsString(is("string body"))
            .contentType(startsWith("text/plain")).and()
          .from()
            .address(contains("test.sender@sii.fr")).and()
          .to()
            .address(containsInAnyOrder("recipient.to1@sii.fr", "recipient.to2@sii.fr", "recipient.to3@sii.fr")).and()
          .cc()
            .address(containsInAnyOrder("recipient.cc1@sii.fr", "recipient.cc2@sii.fr"));
  }
}
1 Define 3 to recipients
2 Define 2 cc recipients
3 Define a single bcc recipient

The email addresses are trimmed:

additional.setProperty("ogham.email.to.default-value", "  foo@example.com  ,    John Doe <bar@example.com>,   abc@example.com");

The email addresses will be:

3.1.8. Provide SMTP authentication

3.1.8.1. Configure username and password

Some SMTP servers need credentials. When using Java Mail API, you need to provide an Authenticator.

Ogham has a shortcut to declare default authentication mechanism using a username and a password. Just set the two following properties:

  • ogham.email.javamail.authenticator.username

  • ogham.email.javamail.authenticator.password

It will automatically create an Authenticator with the provided values.

3.1.8.2. Custom Authenticator
  • Explain how to add a custom Authenticator implementation

3.1.9. Use SSL

  • Add samples for ssl

3.1.10. Send email through GMail

This sample shows how to send a basic email through GMail. Sending through GMail is simply using username/password authentication and enabling SSL.

java logo Java
package fr.sii.ogham.sample.standard.email.gmail;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class GmailSSLBasicSample {

  public static void main(String[] args) throws MessagingException {
    // Instantiate the messaging service using default behavior and
    // provided properties (properties can be externalized)
    MessagingService service = MessagingBuilder.standard()                                      (1)
      .environment()
        .properties()                                                                       (2)
          .set("mail.smtp.auth", true)                                                    (3)
          .set("mail.smtp.host", "smtp.gmail.com")                                        (4)
          .set("mail.smtp.port", 465)                                                     (5)
          .set("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory")         (6)
          .set("ogham.email.javamail.authenticator.username", "<your gmail username>")    (7)
          .set("ogham.email.javamail.authenticator.password", "<your gmail password>")    (8)
          .set("ogham.email.from.default-value", "<your gmail address>")                  (9)
          .and()
        .and()
      .build();
    // send the mail using fluent API
    service.send(new Email()                                                                    (10)
            .subject("GmailSSLBasicSample")
            .body().string("email content")
            .to("ogham-test@yopmail.com"));
  }
}
1 Use the standard builder to configure and instantiate the MessagingService as usual
2 Use the fluent API to provide configuration properties (this is exactly the same as providing java.util.Properties object)
3 GMail requires authentication so property mail.smtp.auth must be set to true (this is the standard JavaMail behavior)
4 GMail SMTP host (using JavaMail standard property)
5 GMail SMTP port (using JavaMail standard property)
6 GMail uses SSL (JavaMail requires this special property to enable SSL)
7 Provide your GMail username
8 Provide your Gmail password
9 Provide your GMail email address
10 Send the email

Using GMail server is quite easy. This samples shows several things:

  1. The code to send email is still the same.

  2. The properties can be provided in a fluent way.

  3. SSL is enabled using standard JavaMail property.

  4. Authentication is done using properties. JavaMail doesn’t provide this shortcut. Without Ogham, you have to implement an Authenticator.

  • Explain that GMail now doesn’t allow to use account password. It requires an application password

  • Show how to create application password (or just link to guide or documentation)

  • Show sample with port 587

3.1.11. Sending email through SendGrid

Sending an email using SendGrid HTTP API is exactly the same in term of code. The only difference is the configuration of Ogham.

java logo Java (SendGrid)
package fr.sii.ogham.sample.standard.email.sendgrid;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

/**
 * This sample shows how to send email with following characteristics:
 * <ul>
 * <li>Use templates</li>
 * <li>Use template prefix</li>
 * <li>The HTML template uses external CSS and images</li>
 * <li>The HTML template loads page fragments</li>
 * <li>The subject is extracted from templates</li>
 * <li>Send HTML email with text fallback</li>
 * <li>Add attachments to the email</li>
 * <li>Properties are loaded from external file and API key is set in code</li>
 * </ul>
 *
 * <p>
 * The templates are available in src/main/resources/template/thymeleaf/email:
 * <ul>
 * <li>full.html</li>
 * <li>full.txt</li>
 * </ul>
 *
 * <p>
 * The HTML template uses a page fragment that is available in
 * src/main/resources/template/thymeleaf/email/fragments/header.html.
 *
 * <p>
 * The HTML template also references external CSS and images that are available
 * in src/main/resources/resources.
 *
 * @author Aurélien Baudet
 *
 */
public class SendGridFullSample {
  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sendgrid-template.properties")                (1)
          .and()
        .build();
    // send the email using fluent API
    // @formatter:off
    service.send(new Email()
            .body().template("full", new SimpleBean("foo", 42))     (2)
            .to("ogham-test@yopmail.com")
            .attach().resource("/attachment/test.pdf"));
    // @formatter:on
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Load properties from a file that is in the classpath.
2 Use a resource present in the classpath.
properties Properties (SendGrid)
# ogham additional properties
ogham.email.sendgrid.api-key=<your sendgrid API key>           (1)
ogham.email.from.default-value=<sender email address>          (2)
ogham.email.template.path-prefix=/template/thymeleaf/email/    (3)
1 The SendGrid API key instead of SMTP configuration
2 The sender email address (same as JavaMail)
3 A prefix for all template paths. In this example, the template paths are /template/thymeleaf/email/full.html and /template/thymeleaf/email/full.txt (same as JavaMail).
java logo Java (JavaMail)
package fr.sii.ogham.sample.standard.email;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

/**
 * This sample shows how to send email with following characteristics:
 * <ul>
 * <li>Use templates</li>
 * <li>Use template prefix</li>
 * <li>The HTML template uses external CSS and images</li>
 * <li>The HTML template loads page fragments</li>
 * <li>The subject is extracted from templates</li>
 * <li>Send HTML email with text fallback</li>
 * <li>Add attachments to the email</li>
 * <li>Properties are loaded from external file</li>
 * </ul>
 *
 * <p>
 * The templates are available in src/main/resources/template/thymeleaf/email:
 * <ul>
 * <li>full.html</li>
 * <li>full.txt</li>
 * </ul>
 *
 * <p>
 * The HTML template uses a page fragment that is available in
 * src/main/resources/template/thymeleaf/email/fragments/header.html.
 *
 * <p>
 * The HTML template also references external CSS and images that are available
 * in src/main/resources/resources.
 *
 * @author Aurélien Baudet
 *
 */
public class FullSample {
  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/email-template.properties")                   (1)
          .and()
        .build();
    // send the email using fluent API
    // @formatter:off
    service.send(new Email()
            .body().template("full", new SimpleBean("foo", 42))     (2)
            .to("ogham-test@yopmail.com")
            .attach().resource("/attachment/test.pdf"));
    // @formatter:on
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Load properties from a file that is in the classpath.
2 Use a resource present in the classpath.
properties Properties (JavaMail)
# general SMTP server
mail.smtp.host=<your server host>                                   (1)
mail.smtp.port=<your server port>                                   (2)
# using with Gmail
#mail.smtp.auth=true
#mail.smtp.host=smtp.gmail.com
#mail.smtp.port=465
#mail.smtp.socketFactory.port=465
#mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
#ogham.email.javamail.authenticator.username=<your gmail username>
#ogham.email.javamail.authenticator.password=<your gmail password>


# ogham additional properties
ogham.email.from.default-value=<sender email address>               (3)
ogham.email.template.path-prefix=/template/thymeleaf/email/         (4)
1 The SMTP host used by JavaMail
2 The SMTP port used by JavaMail
3 The sender email address
4 A prefix for all template paths. In this example, the template paths are /template/thymeleaf/email/full.html and /template/thymeleaf/email/full.txt.

3.1.12. Internationalization

  • Add sample to show how to use LocaleContext

  • Explain ideas and future work about locale manamement

3.2. Send SMS

The samples are available in the sample-standard-usage sub-project.

All samples shown bellow are using SMPP for sending SMS and are also valid for send SMS using any other protocol/API. Only the configuration part differs.

See Sending SMS through OVH to know how to configure Ogham for sending SMS using OVH HTTP API.

SMPP protocol

The 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 advanced configuration.

3.2.1. First SMS using an existing SMPP server

This sample defines two properties mandatory (system ID and password) by this protocol in order to use it.

java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

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", "<your server host>");                                 (1)
    properties.setProperty("ogham.sms.smpp.port", "<your server port>");                                 (2)
    properties.setProperty("ogham.sms.smpp.system-id", "<your server system ID>");                       (3)
    properties.setProperty("ogham.sms.smpp.password", "<your server password>");                         (4)
    properties.setProperty("ogham.sms.from.default-value", "<phone number to display for the sender>");  (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

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 use a configuration file.

3.2.2. Use a template for SMS content

This sample shows how to send a SMS with a content following a template engine language.

java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

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", "<your server host>");
    properties.setProperty("ogham.sms.smpp.port", "<your server port>");
    properties.setProperty("ogham.sms.smpp.system-id", "<your server system ID>");
    properties.setProperty("ogham.sms.smpp.password", "<your server password>");
    properties.setProperty("ogham.sms.from.default-value", "<phone number to display for the sender>");
    // 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 {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
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
thymeleaf ThymeLeaf template
[[${name}]] [[${value}]]

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 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.

3.2.3. Send a long SMS

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:

java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

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", "<your server host>");
    properties.setProperty("ogham.sms.smpp.port", "<your server port>");
    properties.setProperty("ogham.sms.smpp.system-id", "<your server system ID>");
    properties.setProperty("ogham.sms.smpp.password", "<your server password>");
    properties.setProperty("ogham.sms.from.default-value", "<phone number to display for the sender>");
    // 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"));
  }

}
Message length depends on encoding

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

By default, Ogham sends the SMS using 8-bit encoding.

  • explain how to use different encoding

  • link to advanced configuration section

3.2.4. Sending SMS through SmsGlobal

You can send SMS using SmsGlobal SMPP server:

java logo Java
package fr.sii.ogham.sample.standard.sms.smsglobal;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSmsglobalSmppSample {
  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", "smpp.smsglobal.com");                                    (1)
    properties.setProperty("ogham.sms.smpp.system-id", "<your smsglobal username available in API keys>");  (2)
    properties.setProperty("ogham.sms.smpp.password", "<your smsglobal password available in API keys>");   (3)
    properties.setProperty("ogham.sms.from.default-value", "<phone number to display for the sender>");     (4)
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()                                                  (5)
        .environment()
          .properties(properties)                                                                     (6)
          .and()
        .build();                                                                                       (7)
    // send the sms using fluent API
    service.send(new Sms()                                                                                  (8)
        .message().string("sms content")
        .to("+262693493696"));
  }
}
1 The SmsGlobal server host
2 Your SmsGlobal username
3 Your SmsGlobal password
4 The sender phone number
5 Use the standard builder to configure and instantiate the MessagingService as usual
6 Provide configuration properties to Ogham as usual
7 Instantiate the service as usual
8 Send the SMS as usual

You can notice that SMPP server port is not provided. This is because Ogham provides predefined configuration for well-known service providers like SmsGlobal.

3.2.5. Sending SMS through OVH

You can send SMS using OVH HTTP API:

  • Add a sample

  • Split of long messages

3.2.6. Globally configure default sender phone number

Ogham lets you set the sender phone number directly into properties. This phone number is automatically used for all sent SMS.

If you explicitly specify the sender phone number, this value is used instead of the global one:

java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class OverrideDefaultSenderSample {
  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("ogham.sms.smpp.host", "<your server host>");
    properties.put("ogham.sms.smpp.port", "<your server port>");
    properties.setProperty("ogham.sms.smpp.system-id", "<your server system ID>");
    properties.setProperty("ogham.sms.smpp.password", "<your server password>");
    properties.put("ogham.sms.from.default-value", "+33699999999");     (1)
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties(properties)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()                                              (2)
        .message().string("sms content")
        .to("+33752962193"));
    // => the sender phone number is +33699999999

    service.send(new Sms()
        .message().string("sms content")
        .from("+33700000000")                                       (3)
        .to("+33752962193"));
    // => the sender phone number is now +33700000000
  }
}
1 Set the default sender phone number globally using properties
2 Do not provide from field so the sender phone number is +33699999999
3 Override the default sender phone number by providing a from field. The phone number is now +33700000000

3.3. Resource handling

3.3.1. Resource resolution

Resource resolution is used to locate files using a simple path. For example, the path of a file you want to use (template, image, CSS, attachment…​) could be foo/bar/aaa.b. But the file could be located:

  • either in the classpath

  • or on the file system

  • or anywhere else (could be in a database, on a HTTP endpoint…​)

Ogham provides resource resolution abstraction. Any path can contain an information used to indicate which resolver to use to find and read the file content. For example, if the previous path stands for a file that is in the classpath, the Ogham path is classpath:foo/bar/aaa.b. On the contrary, if the path represents a file that is on file system, the Ogham path is file:foo/bar/aaa.b. In both cases, the path is prefixed by a string named lookup prefix (respectively classpath: and file:).

Ogham configures by default (through MessagingBuilder.standard() or MessagingBuilder.minimal()) several resource resolvers:

  • A resolver that is able to locate and read files from the file system with lookup prefix file:.

  • A resolver that is able to locate and read files from the classpath with lookup prefix classpath:.

  • A resolver that doesn’t load file from path but directly uses the string as content with lookups string: or s:.

  • A default resolver with no lookup that is able to locate and read files from the classpath.

Each resolver that is able to handle a path may also handle a path prefix and a path suffix. This is useful in order to provide only a subset of the path (only the file name for example) to Ogham and let Ogham find the real path of the file. For example if you configure Ogham with the prefix foo/bar and .b suffix for both classpath and file resolvers, you can ask Ogham to find the file foo/bar/aaa.b using the path classpath:aaa or file:aaa. Prefix and suffix can be changed using configuration properties (when using MessagingBuilder.standard() or MessagingBuilder.minimal()). There exists one property by message type (email or SMS), by resolver type (classpath or file), by template engine (ThymeLeaf or FreeMarker). Ogham also provides shared configuration properties (configure once for all):

Template engine Message type Resolver type Properties (ordered by higher priority)

ThymeLeaf

Email

Classpath

  • ogham.email.thymeleaf.classpath.path-prefix

  • ogham.email.template.classpath.path-prefix

  • ogham.email.thymeleaf.path-prefix

  • ogham.email.template.path-prefix

  • ogham.template.path-prefix

ThymeLeaf

Email

File

  • ogham.email.thymeleaf.file.path-prefix

  • ogham.email.template.file.path-prefix

  • ogham.email.thymeleaf.path-prefix

  • ogham.email.template.path-prefix

  • ogham.template.path-prefix

ThymeLeaf

SMS

Classpath

  • ogham.sms.thymeleaf.classpath.path-prefix

  • ogham.sms.template.classpath.path-prefix

  • ogham.sms.thymeleaf.path-prefix

  • ogham.sms.template.path-prefix

  • ogham.template.path-prefix

ThymeLeaf

SMS

File

  • ogham.sms.thymeleaf.file.path-prefix

  • ogham.sms.template.file.path-prefix

  • ogham.sms.thymeleaf.path-prefix

  • ogham.sms.template.path-prefix

  • ogham.template.path-prefix

FreeMarker

Email

Classpath

  • ogham.email.freemarker.classpath.path-prefix

  • ogham.email.template.classpath.path-prefix

  • ogham.email.freemarker.path-prefix

  • ogham.email.template.path-prefix

  • ogham.template.path-prefix

FreeMarker

Email

File

  • ogham.email.freemarker.file.path-prefix

  • ogham.email.template.file.path-prefix

  • ogham.email.freemarker.path-prefix

  • ogham.email.template.path-prefix

  • ogham.template.path-prefix

FreeMarker

SMS

Classpath

  • ogham.sms.freemarker.classpath.path-prefix

  • ogham.sms.template.classpath.path-prefix

  • ogham.sms.freemarker.path-prefix

  • ogham.sms.template.path-prefix

  • ogham.template.path-prefix

FreeMarker

SMS

File

  • ogham.sms.freemarker.file.path-prefix

  • ogham.sms.template.file.path-prefix

  • ogham.sms.freemarker.path-prefix

  • ogham.sms.template.path-prefix

  • ogham.template.path-prefix

  • Explain that path prefix/suffix can be useful to do something like aliases

  • Explain that only configuration (not code) need to be updated

  • Add code samples to show how to switch from classpath in tests to files in production

  • Add dedicated section ?

3.3.2. Resources referenced in templates

  • Explain that Ogham supports relative resources

  • Explain that relative resources are relative to the lookup mode (file:, classpath:, …​)

  • Add note for Thymeleaf fragments (extension needed for now)

3.4. Properties handling

Property configuration is a good way to separate code from configuration. Ogham allows you to configure values using builders. For example, you can configure the SMTP host and port like this:

java logo Java
MessagingBuilder.standard()
  .email()
    .sender(JavaMailBuilder.class)
      .host("localhost")
      .port(25);

However, when using a library, it is simpler that this library provides a way to configure some values without the need of writing code to configure it. The integration is easier. The configuration should also be done in several ways to let the developer choose what fits his needs.

You can use configuration properties that are defined several ways:

  • in a properties file that is present in the classpath (inside the application)

  • in a properties file that is present on the file system (outside the application)

  • using standard java.util.Properties

  • using standard System.getProperties()

  • define properties directly in code in a fluent way

java logo Java
package fr.sii.ogham.sample.standard;

import java.io.IOException;
import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;

public class PropertiesSample {
  public static void main(String[] args) throws MessagingException, IOException {
    Properties props = new Properties();
    props.setProperty("ogham.email.from.default-value", "hello@world.com");
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties(props)                                   (1)
          .properties("classpath:email.properties")            (2)
          .properties("file:/etc/app/email.properties")        (3)
          .properties()
            .set("mail.smtp.port", "10")                     (4)
            .and()
          .systemProperties()                                  (5)
          .and()
        .build();
    service.send(/*your message here*/null);
  }
}
1 Use standard java.util.Properties
2 Load the file from the classpath (relative to root of the classpath)
3 Load the file from the file system
4 Directly set a property with its value in a fluent way
5 Use standard System.getProperties()

This sample shows that configuration may come from several sources (code, shared properties, file inside the application, file outside the application and from system properties).

3.4.1. Properties priority

If you define properties using several different ways, you may have the same key several times. In that case, which value will be used by Ogham ?

By default (using MessagingBuilder.standard() or MessagingBuilder.minimal()), Ogham defines the following order (first is used if defined):

  • .systemProperties()

  • .properties("file:..")

  • .properties(new Properties()) or .properties().set(…​)

  • .properties("classpath:…​")

3.4.1.1. Why this order ?

Configuration outside the application should override configuration that is inside the application. This is necessary if you want to be able to deploy the same application in different environments without changing code or needing to rebuild with a different profile.

For configuration outside the application, system properties (defined either in system environment or using java command line arguments) has higher priority than a configuration file outside the application. This is useful to have a configuration file that is shared between several applications or instances and override only some values explicitly.

For configuration inside the application, code has higher priority than configuration defined in a classpath file. This is useful to define global configuration for the application using a configuration file and let the possibility to override explicitly some values in code. This is useful in tests for example.

java logo Java
package fr.sii.ogham.sample.standard;

import java.io.IOException;
import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;

public class PropertiesSample {
  public static void main(String[] args) throws MessagingException, IOException {
    Properties props = new Properties();
    props.setProperty("ogham.email.from.default-value", "hello@world.com");
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties(props)                                   (1)
          .properties("classpath:email.properties")            (2)
          .properties("file:/etc/app/email.properties")        (3)
          .properties()
            .set("mail.smtp.port", "10")                     (4)
            .and()
          .systemProperties()                                  (5)
          .and()
        .build();
    service.send(/*your message here*/null);
  }
}
1 Use standard java.util.Properties
2 Load the file from the classpath (relative to root of the classpath)
3 Load the file from the file system
4 Directly set a property with its value in a fluent way
5 Use standard System.getProperties()
properties classpath:email.properties
mail.smtp.host=localhost
mail.smtp.port=25
ogham.email.from.default-value=foo@test.com
properties file:/etc/app/email.properties
mail.smtp.host=smtp.domain.com
Result

Running this sample with the following command line:

$ java -Dogham.email.from.default-value="bar@domain.com" fr.sii.ogham.sample.standard.PropertiesSample

Gives the following property values:

Property Result value

mail.smtp.host

smtp.domain.com

mail.smtp.port

10

ogham.email.from.default-value

bar@domain.com

3.4.1.2. What happens if there are several values for the same property and for the same source ?

For example, if you use two configuration files defined in the classpath:

java logo Java
  MessagingService service = MessagingBuilder.standard()
      .environment()
        .properties("classpath:common.properties")
        .properties("classpath:email.properties")
        .and()
      .build();
properties common.properties
mail.smtp.host=localhost
mail.smtp.port=25
properties email.properties
mail.smtp.host=smtp.domain.com
Result
Property Result value

mail.smtp.host

smtp.domain.com

mail.smtp.port

25

For the same level of priority, this is the declaration order that prevails.

3.4.1.3. How to use custom priorities ?

If you want to use a different priority order, you can explicitly register properties with a custom priority:

java logo Java
  MessagingService service = MessagingBuilder.standard()
      .environment()
        .properties("classpath:common.properties", 2)
        .properties("classpath:email.properties", 1)
        .and()
      .build();
properties common.properties
mail.smtp.host=localhost
mail.smtp.port=25
properties email.properties
mail.smtp.host=smtp.domain.com
Result
Property Result value

mail.smtp.host

localhost

mail.smtp.port

25

You can notice that the result is now different than using default priorities.

Default priority values are:

  • Using .systemProperties(): 100000

  • Load property file from the filesystem (properties("file:…​"): 90000

  • Using custom java.util.Properties object (properties(new Properties())): 80000

  • Using custom properties through .properties() fluent API: 80000

  • Load property file from the classpath (properties("classpath:…​")): 70000

3.4.2. Files location

By default, Ogham automatically loads configuration files placed in:

  • config/ogham.properties relative to current directory

  • config/application.properties relative to current directory

  • config/ogham.properties in the classpath

  • config/application.properties in the classpath

This way, developer don’t need to add code to explicitly load some configuration files:

java logo Use default files
package fr.sii.ogham.sample.standard;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;

public class DefaultPropertiesLocationSample {
  public static void main(String[] args) throws MessagingException {
    /**
     * By default, Ogham loads files from:
     * - config/ogham.properties and config/application.properties
     *   from classpath if exists
     * - config/ogham.properties and config/application.properties
     *   external files from current directory if exists
     */
    MessagingService service = MessagingBuilder.standard()
        .build();
    service.send(/*your message here*/null);
  }
}
java logo Manual equivalence
package fr.sii.ogham.sample.standard;

import java.io.IOException;
import java.util.Properties;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;

public class PropertiesSample {
  public static void main(String[] args) throws MessagingException, IOException {
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("?file:config/ogham.properties")
          .properties("?file:config/application.properties")
          .properties("?classpath:config/ogham.properties")
          .properties("?classpath:config/application.properties")
          .and()
        .build();
    service.send(/*your message here*/null);
  }
}
Optional location

Using ? character in the path means that the configuration file is optional. It will not fail if the file doesn’t exist.

Without ?, if the file is missing, Ogham will fail at startup to indicate that some configuration is missing.

In addition to default locations, you can specify a different location using ogham.config.location property:

java logo Custom location
package fr.sii.ogham.sample.standard;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;

public class DefaultPropertiesLocationSample {
  public static void main(String[] args) throws MessagingException {
    /**
     * By default, Ogham loads files from:
     * - config/ogham.properties and config/application.properties
     *   from classpath if exists
     * - config/ogham.properties and config/application.properties
     *   external files from current directory if exists
     */
    MessagingService service = MessagingBuilder.standard()
        .build();
    service.send(/*your message here*/null);
  }
}

Running the sample from command line:

cd /var/run
java \
  -Dogham.config.location=file:/etc/your-app \
  ... \
  -jar ...

Will load files from:

  • /etc/your-app/config/ogham.properties (explicit location)

  • /etc/your-app/config/application.properties (explicit location)

  • /var/run/config/ogham.properties (default location relative to current directory)

  • /var/run/config/application.properties (default location relative to current directory)

  • config/ogham.properties in the classpath

  • config/application.properties in the classpath

Several locations

You can set several locations at once by separating each path with a comma (,).

3.4.3. Configuration profiles

In order to have the exact same code but different configurations for different execution environments, Ogham also supports profiles for configuration files.

The aim is to have for example:

  • one configuration file for development (that configures Ogham to send emails using external SMTP server)

  • one configuration file for tests (that configures Ogham to send emails using embedded SMTP server)

  • one configuration file for production (that configures Ogham to send emails through SendGrid)

This can be done by simply appending a suffix to the configuration file name of your choice. For instance, you want three profiles named dev, tests, prod, then you can create the following files:

  • config/ogham-dev.properties and/or config/application-dev.properties

  • config/ogham-tests.properties and/or config/application-tests.properties

  • config/ogham-prod.properties and/or config/application-prod.properties

Each file can have its own configuration.

The next step is to enable a profile. This can be done by setting ogham.profiles.active system property:

java \
  -Dogham.profiles.active=prod \
  ... \
  -jar ...
Enable several profiles

You can enable several profiles at once by separating each profile name with a comma (,).

Default configuration

The default configuration files are still loaded but with lower priority.

For instance if:

  • a property foo with a value bar is defined in default location (config/ogham.properties or config/application.properties)

  • and the same property foo with the value foobar is defined in dev profile (config/ogham-dev.properties or config/application-dev.properties),

the value foobar will be used if dev profile is enabled.

  • Explain conversions from String to target type

  • List already existing conversions

  • Explain how to register custom converter → link to extend.adoc

3.5. Templating

3.5.1. FreeMarker

  • Explain specific features supported by FreeMarker

  • Show samples with static method calls

  • Link to advanced configuration to custom FreeMarker configuration

3.5.2. ThymeLeaf

  • Explain specific features supported by Thymeleaf

  • Show samples with static method calls

  • Link to advanced configuration to custom Thymeleaf configuration

3.5.3. Mixed

  • Explain variants

  • Explain how to register custom variants (advanced configuration ?)

3.6. Using Ogham in a Spring Boot application

Ogham provides auto-configuration modules for Spring Boot (see how to include auto-configuration modules). To use Ogham in Spring, you can directly inject (autowire) MessagingService bean.

In addition to standalone behaviors, Ogham also supports Spring Boot modules and auto-configuration:

  • If spring-boot-starter-thymeleaf is included, Ogham uses Spring ThymeLeaf template engine (using SpringTemplateEngine bean), configuration and properties (spring.thymeleaf properties)

  • If spring-boot-starter-freemarker is included, Ogham uses Spring FreeMarker properties (spring.freemarker properties)

  • If spring-boot-starter-mail is included, Ogham uses Spring mail properties (spring.mail properties)

  • If sendgrid-java is included, Ogham uses Spring SendGrid bean and properties (spring.sendgrid properties)

  • Provide properties metadata for auto-completion

Java Mail auto-configuration

The property ogham.email.javamail.host takes precedence over spring.mail.host property. The property ogham.email.javamail.port takes precedence over spring.mail.port property.

Thymeleaf auto-configuration

If you provide any of:

  • ogham.email.template.path-prefix

  • ogham.email.thymeleaf.classpath.path-prefix

  • ogham.email.template.classpath.path-prefix

  • ogham.email.thymeleaf.path-prefix

  • ogham.email.template.path-prefix

  • ogham.sms.template.path-prefix

  • ogham.sms.thymeleaf.classpath.path-prefix

  • ogham.sms.template.classpath.path-prefix

  • ogham.sms.thymeleaf.path-prefix

  • ogham.sms.template.path-prefix

  • ogham.template.path-prefix

Then spring.thymeleaf.prefix is not used in order to keep coherence between Spring application and non-Spring application. Moreover, this lets you the opportunity to use a prefix for Spring web views and a different prefix for email and/or SMS. The last reason is that Ogham properties are more specific:

  • you can target the message type: use different prefixes for email and SMS

  • you can target the resource resolution: use different prefixes for classpath and file system for example

FreeMarker auto-configuration

If you provide any of:

  • ogham.email.template.path-prefix

  • ogham.email.freemarker.classpath.path-prefix

  • ogham.email.template.classpath.path-prefix

  • ogham.email.freemarker.path-prefix

  • ogham.email.template.path-prefix

  • ogham.sms.template.path-prefix

  • ogham.sms.freemarker.classpath.path-prefix

  • ogham.sms.template.classpath.path-prefix

  • ogham.sms.freemarker.path-prefix

  • ogham.sms.template.path-prefix

  • ogham.template.path-prefix

Then spring.freemarker.prefix is not used in order to keep coherence between Spring application and non-Spring application. Moreover, this lets you the opportunity to use a prefix for Spring web views and a different prefix for email and/or SMS. The other reason is that Ogham properties are more specific:

  • you can target the message type: use different prefixes for email and SMS

  • you can target the resource resolution: use different prefixes for classpath and file system for example

SendGrid auto-configuration

Including all Ogham features adds sendgrid-java dependency. This means that Spring Boot auto-configuration for SendGrid automatically defines the sendGrid bean instance if spring.sendgrid.api-key property is defined. In this case, the sendGrid bean is always used by Ogham. This means that if you provide spring.sendgrid.api-key, the properties ogham.email.sendgrid.api-key and ogham.email.sendgrid.unit-testing won’t be used. Before Spring Boot 2.0.0, spring.sendgrid.username and spring.sendgrid.password can be provided instead of spring.sendgrid.api-key.

3.6.1. Send email

Usage of MessagingService is exactly the same as standalone usage. The only difference is that MessagingService is automatically created and injectable. The following sample shows a Spring Web that exposes one simple endpoint for sending email using Ogham. The sample shows several Ogham features at once:

  • Using both HTML (using ThymeLeaf) and text templates (using FreeMarker)

  • Templates are located in a sub-folder and prefixes for templates are configured using Spring standard properties

  • Using a configuration property to define the sender address

  • The SMTP server host and port are defined using Spring standard properties

  • The email subject is provided by the title tag of the HTML template

java logo Java
package fr.sii.ogham.sample.springboot.email;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

@SpringBootApplication
@PropertySource("application-email-template.properties")  // just needed to be able to run the sample
public class ThymeleafHtmlAndFreemarkerTextSample {

  public static void main(String[] args) throws MessagingException {
    SpringApplication.run(ThymeleafHtmlAndFreemarkerTextSample.class, args);
  }

  @RestController
  public static class EmailController {
    // Messaging service is automatically created using Spring Boot features
    // The configuration can be set into application-email-template.properties
    // The configuration files are stored into src/main/resources
    // The configuration file set the prefix for templates into email folder available in src/main/resources
    @Autowired
    MessagingService messagingService;                                              (1)

    @PostMapping(value="api/email/multitemplate/mixed")
    @ResponseStatus(HttpStatus.CREATED)
    public void sendEmail(@RequestParam("to") String to, @RequestParam("name") String name, @RequestParam("value") int value) throws MessagingException {
      // using fluent API
      messagingService.send(new Email()                                           (2)
                  .body().template("mixed",                           (3)
                            new SimpleBean(name, value))    (4)
                  .to(to));                                           (5)
    }
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Inject Ogham service
2 Use the Ogham service to send an email
3 Use HTML and text templates that are available in classpath. Spring is configured to use a path prefix for both ThymeLeaf and FreeMarker (see properties configuration tab). Both HTML and text templates are then located respectively at src/main/resources/email/mixed.html and src/main/resources/email/mixed.txt/ftl.
4 Use any Java object for evaluating template variables
5 The sender email address that comes from request parameter
thymeleaf HTML template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">        (1)
    <head>
        <title>Subject of the mail</title>        (2)
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title" th:text="${name}"></h1>   (3)
        <p class="text" th:text="${value}"></p>     (4)
    </body>
</html>
1 Include the ThymeLeaf namespace
2 Use the title tag to define the subject of the email
3 Evaluate the name attribute value of SimpleBean in the template
4 Evaluate the value attribute value of SimpleBean in the template
freemarker logo Text template
properties Spring properties
# configuration for email
spring.mail.host=<your server host>                   (1)
spring.mail.port=<your server port>                   (2)
ogham.email.from.default-value=<your gmail address>   (3)
# configuration for template engines
spring.thymeleaf.prefix=/email/                       (4)
spring.freemarker.prefix=/email/                      (5)
1 The SMTP host using Spring property
2 The SMTP port using Spring property
3 The sender address that is declared globally
4 The path prefix for ThymeLeaf templates
5 The path prefix for FreeMarker templates

3.6.2. Send SMS

Usage of MessagingService is exactly the same as standalone usage. The only difference is that MessagingService is automatically created and injectable. The following sample shows a Spring Web that exposes one simple endpoint for sending SMS using Ogham. The sample shows several Ogham features at once:

  • Using text template (using FreeMarker)

  • Templates are located in a sub-folder and prefix for SMS templates is configured through Ogham property

  • SMS template extension is configured globally in order to avoid extension in Java code

  • Using a configuration property to define the sender phone number

  • The SMPP server host, port and authentication are defined using properties

java logo Java
package fr.sii.ogham.sample.springboot.sms;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

@SpringBootApplication
@PropertySource("application-sms-template.properties")  // just needed to be able to run the sample
public class TemplateSample {

  public static void main(String[] args) throws MessagingException {
    SpringApplication.run(TemplateSample.class, args);
  }

  @RestController
  public static class SmsController {
    // Messaging service is automatically created using Spring Boot features
    // The configuration can be set into application-sms-template.properties
    // The configuration files are stored into src/main/resources
    @Autowired
    MessagingService messagingService;                                              (1)

    @PostMapping(value="api/sms/template")
    @ResponseStatus(HttpStatus.CREATED)
    public void sendSms(@RequestParam("to") String to, @RequestParam("name") String name, @RequestParam("value") int value) throws MessagingException {
      // send the SMS using fluent API
      messagingService.send(new Sms()                                             (2)
                  .message().template("register",                     (3)
                            new SimpleBean(name, value))    (4)
                  .to(to));                                           (5)
    }
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Inject Ogham service
2 Use the Ogham service to send a SMS
3 Use a text template as SMS content
4 Use any Java object for evaluating template variables
5 The sender phone number that comes from request parameter
freemarker logo Template
Hello ${name}, the confirmation code is ${value}
properties Spring properties
# ogham configuration for SMS
ogham.sms.smpp.host=<your server host>                                  (1)
ogham.sms.smpp.port=<your server port>                                  (2)
ogham.sms.smpp.system-id=<your server system ID>                        (3)
ogham.sms.smpp.password=<your server password>                          (4)
ogham.sms.from.default-value=<phone number to display for the sender>   (5)
ogham.sms.template.path-prefix=/sms/                                    (6)
ogham.sms.template.path-suffix=.txt.ftl                                 (7)
1 The SMPP host
2 The SMPP port
3 The SMPP system ID (account)
4 The SMPP password
5 The sender phone number that is declared globally
6 The path prefix for SMS templates
7 The path suffix for SMS templates (FreeMarker extension in this case)

4. Testing

4.1. Include test utilities

Maven
Add ogham-test-utils dependency to your pom.xml
<dependency>
  <groupId>fr.sii.ogham</groupId>
  <artifactId>ogham-test-utils</artifactId>
  <version>3.0.0</version>
  <scope>test</scope>
</dependency>
Gradle
Add ogham-test-utils to your build.gradle
dependencies {
  testImplementation 'fr.sii.ogham:ogham-test-utils:3.0.0'
}

This will include:

4.1.1. Configure static method imports for Eclipse

Ogham uses Hamcrest, Mockito and also provides test utilities. Those utilities provide many static methods. Static methods are convenient for code readability. However, Eclipse needs some configuration to help you to import static methods with autocompletion.

In Eclipse, select Window  Preferences. In the preferences window, select Java  Editor  Content Assist  Favorites.

static import empty
Figure 1. Preferences window

To add all static methods of a class, click on New Type…​.

static import newtype
Figure 2. Add all static methods and attributes of a class

Type the class that contains the static methods (you can also click the browse button to search classes from classpath).

static import newtype search
Figure 3. Example for Ogham assertions
static import search
Figure 4. Search for a class

Click on OK

Recommended imports to add:

  • fr.sii.ogham.assertion.OghamAssertions.*

  • org.hamcrest.Matchers.*

  • org.hamcrest.CoreMatchers.*

Optional imports that may be useful:

  • fr.sii.ogham.assertion.OghamMatchers.*

  • fr.sii.ogham.util.ResourceUtils.*

  • org.mockito.ArgumentMatchers.*

  • org.mockito.Mockito.*

  • org.junit.* (if you are using JUnit 4)

  • org.junit.Assert.* (if you are using JUnit 4)

  • org.junit.matchers.JUnitMatchers.* (if you are using JUnit 4)

  • org.junit.Assume.* (if you are using JUnit 4)

  • org.junit.jupiter.api.Assertions.* (if you are using JUnit 5)

  • org.junit.jupiter.api.Assumptions.* (if you are using JUnit 5)

  • org.assertj.core.api.Assertions.* (if you are using AssertJ)

static import all
Figure 5. Result with all static imports described above

Once you have added all static imports you need, click on OK.

Now you can use autocompletion and Eclipse will automatically add the static import.

static import autocomplete
Figure 6. Autocompletion now suggests static methods
Eclipse shortcut

Eclipse can rewrite a call to a static method prefixed by class. For example in your code you have OghamAssertions.assertThat, pressing Ctrl+M shortcut (cursor must be placed in the method name), Eclipse will add the static import and the code is just assertThat.

4.2. Testing emails

4.2.1. First test

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 GreenMail as local SMTP server.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

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 <test.sender@sii.fr>")
            .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 <recipient@sii.fr>"));
    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

4.2.2. Testing HTML message

Comparing two HTML documents can be tricky. Indeed, the HTML attributes can be declared in a different order, number of spaces/tabs can be different, some attributes may be declared differently but corresponding to the same behavior (for example disabled attribute can be declared only disabled with no value, disabled="true" or disabled="disabled").

Ogham provides two distinct matchers to check if:

  • HTML is identical (exactly the same nodes at same position and same attributes with same values)

  • HTML is similar (nodes may be at different positions, same attributes with same values)

In addition to comparison helpers, Ogham provides helpers to load files from classpath in tests (ResourceUtils.resource and ResourceUtils.resourceAsString). This is useful to avoid writing expected HTML content as string in your code and also avoid writing the same utility function every time.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static fr.sii.ogham.testing.assertion.OghamMatchers.isIdenticalHtml;
import static fr.sii.ogham.testing.util.ResourceUtils.resourceAsString;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class EmailHtmlTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void registerMessage() throws MessagingException, javax.mail.MessagingException, IOException {
    // @formatter:off
    oghamService.send(new Email()
                .body().template("/template/register.html",                         (1)
                              new SimpleBean("foo", 42))              (2)
                .to("Recipient Name <recipient@sii.fr>"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("foo - Confirm your registration"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .to()
          .address(hasItems("recipient@sii.fr"))
          .personal(hasItems("Recipient Name")).and()
        .body()
          .contentAsString(isIdenticalHtml(resourceAsString("/expected/register.html")))  (3)
          .contentType(startsWith("text/html")).and()
        .alternative(nullValue())
        .attachments(emptyIterable());
    // @formatter:on
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Use an HTML template
2 Object used to evaluate variables in the template
3 Assert that HTML is similar as an expected HTML content
thymeleaf HTML template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
 <head>
  <title th:text="|${name} - Confirm your registration|"></title>
  <meta charset="utf-8" />
 </head>
 <body>
  <h1 title="foo" class="title" th:text="|Hello ${name}|"></h1>

  <p class="text" th:text="${value}">
  </p>
 </body>
</html>
html Expected HTML
<!DOCTYPE html>
<html>
  <head>
    <title>foo - Confirm your registration</title>
    <meta charset="utf-8"></meta>
  </head>
    <body>
    <h1 class="title" title="foo">Hello foo</h1>
    <p class="text">42</p>
  </body>
</html>

4.2.3. Help in Eclipse for failing tests

Comparing two HTML documents can be tricky. Indeed, the HTML attributes can be declared in a different order, number of spaces/tabs can be different, some attributes may be declared differently but corresponding to the same behavior (for example disabled attribute can be declared only disabled with no value, disabled="true" or disabled="disabled").

Ogham provides two distinct matchers to check if:

  • HTML is identical (exactly the same nodes at same position and same attributes with same values)

  • HTML is similar (nodes may be at different positions, same attributes with same values)

In addition to comparison helpers, Ogham provides helpers to load files from classpath in tests (ResourceUtils.resource and ResourceUtils.resourceAsString). This is useful to avoid writing expected HTML content as string in your code and also avoid writing the same utility function every time.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static fr.sii.ogham.testing.assertion.OghamMatchers.isIdenticalHtml;
import static fr.sii.ogham.testing.util.ResourceUtils.resourceAsString;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class EmailHtmlTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void registerMessage() throws MessagingException, javax.mail.MessagingException, IOException {
    // @formatter:off
    oghamService.send(new Email()
                .body().template("/template/register.html",                         (1)
                              new SimpleBean("foo", 42))              (2)
                .to("Recipient Name <recipient@sii.fr>"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("foo - Confirm your registration"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .to()
          .address(hasItems("recipient@sii.fr"))
          .personal(hasItems("Recipient Name")).and()
        .body()
          .contentAsString(isIdenticalHtml(resourceAsString("/expected/register.html")))  (3)
          .contentType(startsWith("text/html")).and()
        .alternative(nullValue())
        .attachments(emptyIterable());
    // @formatter:on
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Use an HTML template
2 Object used to evaluate variables in the template
3 Assert that HTML is similar as an expected HTML content
thymeleaf HTML template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
 <head>
  <title th:text="|${name} - Confirm your registration|"></title>
  <meta charset="utf-8" />
 </head>
 <body>
  <h1 title="foo" class="title" th:text="|Hello ${name}|"></h1>

  <p class="text" th:text="${value}">
  </p>
 </body>
</html>
html Expected HTML
<!DOCTYPE html>
<html>
  <head>
    <title>foo - Confirm your registration</title>
    <meta charset="utf-8"></meta>
  </head>
    <body>
    <h1 class="title" title="foo">Hello foo</h1>
    <p class="text">42</p>
  </body>
</html>
4.2.3.1. Help in Eclipse for failing tests

When your tests report errors due to HTML differences, Ogham helps with Eclipse integration. The following sample is voluntary in error due to differences between actual HTML and expected HTML (this is the expected HTML that is incorrect) in order to show how Ogham helps you.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static fr.sii.ogham.testing.assertion.OghamMatchers.isIdenticalHtml;
import static fr.sii.ogham.testing.util.ResourceUtils.resourceAsString;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class EmailHtmlTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void registerMessage() throws MessagingException, javax.mail.MessagingException, IOException {
    // @formatter:off
    oghamService.send(new Email()
                .body().template("/template/register.html",                         (1)
                              new SimpleBean("foo", 42))              (2)
                .to("Recipient Name <recipient@sii.fr>"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("foo - Confirm your registration"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .to()
          .address(hasItems("recipient@sii.fr"))
          .personal(hasItems("Recipient Name")).and()
        .body()
          .contentAsString(isIdenticalHtml(resourceAsString("/expected/register.html")))  (3)
          .contentType(startsWith("text/html")).and()
        .alternative(nullValue())
        .attachments(emptyIterable());
    // @formatter:on
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Use an HTML template
2 Object used to evaluate variables in the template
3 Assert that HTML is identical as an expected HTML content
thymeleaf HTML template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
 <head>
  <title th:text="|${name} - Confirm your registration|"></title>
  <meta charset="utf-8" />
 </head>
 <body>
  <h1 title="foo" class="title" th:text="|Hello ${name}|"></h1>

  <p class="text" th:text="${value}">
  </p>
 </body>
</html>
html Expected HTML
<!DOCTYPE html>
<html>
  <head>
    <title>bar - Confirm your registration</title>
    <meta charset="utf-8"></meta>
  </head>
    <body>
    <h1 class="title" title="foo">Hello bar</h1>
    <p class="text">42</p>
  </body>
</html>

The expected HTML is voluntary wrong:

  • title starts with bar instead of foo

  • h1 text is Hello bar instead of Hello foo

All other differences are not reported as errors as the resulting HTML interpretation will be the same.

tests junit error
Figure 7. JUnit view

Double-clicking on the exception:

tests junit error open comparison
Figure 8. Exception message is clickable

Opens a comparison window:

tests comparison
Figure 9. Comparison window
Spaces, tag closing and attributes order are reported by the comparison window provided by Eclipse. Indeed, Eclipse just compares two strings (not HTML documents). This window is just an helper to visualize where the problems are. But the real errors to fix are displayed both in logs and in the error summary.
tests junit error useful part
Figure 10. Useful part of the error summary
4.2.3.2. Testing email with an HTML body and text alternative

Testing an email sent with two main parts (HTML and text fallback) is straightforward.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static fr.sii.ogham.testing.assertion.OghamMatchers.isIdenticalHtml;
import static fr.sii.ogham.testing.util.ResourceUtils.resourceAsString;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.equalToCompressingWhiteSpace;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class EmailHtmlAndTextTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void multiTemplateContent() throws MessagingException, javax.mail.MessagingException, IOException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Multi")
                .body().template("template/mixed/simple", new SimpleBean("bar", 42))
                .to("recipient@sii.fr"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("Multi"))
        .from().address(hasItems("test.sender@sii.fr")).and()
        .to().address(hasItems("recipient@sii.fr")).and()
        .body()                                                                                              (1)
          .contentAsString(isIdenticalHtml(resourceAsString("/expected/simple_bar_42.html")))              (2)
          .contentType(startsWith("text/html")).and()                                                      (3)
        .alternative()                                                                                       (4)
          .contentAsString(equalToCompressingWhiteSpace(resourceAsString("/expected/simple_bar_42.txt")))  (5)
          .contentType(startsWith("text/plain")).and()                                                     (6)
        .attachments(emptyIterable());
    // @formatter:on
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Access to main body assertions
2 Assert that main body message is HTML content and is similar as an expected HTML content loaded from classpath
3 Assert that main body message mimetype is text/html
4 Access to alternative assertions
5 Assert that alternative body message is text content and is exactly the expected text content loaded from classpath
6 Assert that alternative body message mimetype is text/plain
thymeleaf HTML template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title" th:text="${name}"></h1>
        <p class="text" th:text="${value}"></p>
    </body>
</html>
freemarker logo Text template
${name} ${value}
html Expected HTML
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title">bar</h1>
        <p class="text">42</p>
    </body>
</html>
txt Expected text
4.2.3.3. Testing attachments

You can also test that attachments are sent correctly.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static fr.sii.ogham.testing.assertion.util.EmailUtils.ATTACHMENT_DISPOSITION;
import static fr.sii.ogham.testing.util.ResourceUtils.resource;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class EmailAttachmentTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void simple() throws MessagingException, javax.mail.MessagingException, IOException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Test")
                .body().string("body")
                .to("recipient@sii.fr")
                .attach().resource("/attachment/test.pdf"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("Test"))
        .from().address(hasItems("test.sender@sii.fr")).and()
        .to().address(hasItems("recipient@sii.fr")).and()
        .body()
          .contentAsString(is("body"))
          .contentType(startsWith("text/plain")).and()
        .alternative(nullValue())
        .attachments(hasSize(1))                              (1)
        .attachment(0)                                        (2)
          .content(is(resource("/attachment/test.pdf")))    (3)
          .contentType(startsWith("application/pdf"))       (4)
          .filename(is("test.pdf"))                         (5)
          .disposition(is(ATTACHMENT_DISPOSITION));         (6)
    // @formatter:on
  }
}
1 Assert that exactly one attachment is attached to the received email
2 Access the first attachment for defining assertions on it
3 Assert that the first received file is exactly the same as the sent one
4 Assert that the detected mimetype of the first attachment is correct
5 Assert that the name of the first attachment is the expected one
6 Assert that the attachment disposition is correct
4.2.3.4. Testing several emails at once

When you send an email to several recipients, there is one message per recipient. So if you send an email to 6 recipients, you may need to ensure that the 6 messages are received correctly. Ogham provides the every method to apply defined assertions on all messages.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class SeveralRecipientsTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void severalRecipients() throws MessagingException, javax.mail.MessagingException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Simple")
                .body().string("string body")
                .to("recipient1@sii.fr", "recipient2@sii.fr", "recipient3@sii.fr")
                .cc("recipient4@sii.fr", "recipient5@sii.fr")
                .bcc("recipient6@sii.fr"));
    assertThat(greenMail).receivedMessages()
      .count(is(6))                                                               (1)
      .every()                                                                    (2)
        .subject(is("Simple"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .to()
          .address(containsInAnyOrder("recipient1@sii.fr",                    (3)
                        "recipient2@sii.fr",
                        "recipient3@sii.fr")).and()
        .cc()
          .address(containsInAnyOrder("recipient4@sii.fr",                    (4)
                        "recipient5@sii.fr")).and()
        .body()
          .contentAsString(is("string body"))
          .contentType(startsWith("text/plain")).and()
        .alternative(nullValue())
        .attachments(emptyIterable());
    // @formatter:on
  }
}
1 Assert that 6 distinct messages are received
2 every applies all later defined assertions to all messages (the 6 messages)
3 Assert that each received messages has exactly 3 to recipients
4 Assert that each received messages has exactly 2 cc recipients

The bcc recipient is not testable. Indeed, bcc recipients are hidden recipients that must not be visible. So the email is received (there are 6 received messages not 5). But bcc field of each email are empty because they are not present in the sent email. The bcc field is just used for routing.

Sometimes you also need to send several different emails. The emails may have some identical information (sender or subject for example). So you can mix every (define assertions all messages) with message (define assertion for one particular message).

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class SeveralEmailsTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void severalDisctinctMessages() throws MessagingException, javax.mail.MessagingException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Simple")
                .body().string("string body 1")
                .to("recipient1@sii.fr"));
    oghamService.send(new Email()
                .subject("Simple")
                .body().string("string body 2")
                .to("recipient2@sii.fr"));
    oghamService.send(new Email()
                .subject("Simple")
                .body().string("string body 3")
                .to("recipient3@sii.fr"));
    assertThat(greenMail).receivedMessages()
      .count(is(3))
      .every()                                                   (1)
        .subject(is("Simple"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .body()
          .contentType(startsWith("text/plain")).and()
        .alternative(nullValue())
        .attachments(emptyIterable())
        .and()
      .message(0)                                                (2)
        .body()
          .contentAsString(is("string body 1"))
          .and()
        .to()
          .address(hasItems("recipient1@sii.fr"))
          .and()
        .and()
      .message(1)                                                (3)
        .body()
          .contentAsString(is("string body 2"))
          .and()
        .to()
          .address(hasItems("recipient2@sii.fr"))
          .and()
        .and()
      .message(2)                                                (4)
        .body()
          .contentAsString(is("string body 3"))
          .and()
        .to()
          .address(hasItems("recipient3@sii.fr"));
    // @formatter:on
  }
}
1 Shared assertions (subject, sender and body mimetype)
2 Specific assertions for first sent message (recipient and message content)
3 Specific assertions for second sent message (recipient and message content)
4 Specific assertions for third sent message (recipient and message content)
4.2.3.5. Testing with SMTP authentication

GreenMail allows to start the SMTP server with user and credentials.

java logo Java
package oghamall.it.email;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.testing.extension.junit.LoggingTestRule;
import fr.sii.ogham.testing.extension.junit.email.RandomPortGreenMailRule;

public class EmailSMTPAuthenticationTest {
  private MessagingService oghamService;

  @Rule
  public final LoggingTestRule loggingRule = new LoggingTestRule();

  @Rule
  public final GreenMailRule greenMail = new RandomPortGreenMailRule();

  @Before
  public void setUp() throws IOException {
    greenMail.setUser("test.sender@sii.fr", "test.sender", "password");             (1)
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties("/application.properties")
          .properties()
            .set("mail.smtp.host", greenMail.getSmtp().getBindTo())
            .set("mail.smtp.port", greenMail.getSmtp().getPort())
            .set("mail.smtp.auth", "true")                        (2)
            .set("ogham.email.javamail.authenticator.username", "test.sender")      (3)
            .set("ogham.email.javamail.authenticator.password", "password")       (4)
            .and()
          .and()
        .build();
  }

  @Test
  public void authenticated() throws MessagingException, javax.mail.MessagingException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Simple")
                .content("string body")
                .to("Recipient Name <recipient@sii.fr>"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("Simple"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .to()
          .address(hasItems("recipient@sii.fr"))
          .personal(hasItems("Recipient Name")).and()
        .body()
          .contentAsString(is("string body"))
          .contentType(startsWith("text/plain")).and()
        .alternative(nullValue())
        .attachments(emptyIterable());
    // @formatter:on
  }
}
1 Configure GreenMail to register a user
2 Configure JavaMail to enable authentication
3 Configure Ogham to provide the authentication username
4 Configure Ogham to provide the authentication password
Only the setup differs, the test is the same.

4.2.4. Testing email with an HTML body and text alternative

Testing an email sent with two main parts (HTML and text fallback) is straightforward.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static fr.sii.ogham.testing.assertion.OghamMatchers.isIdenticalHtml;
import static fr.sii.ogham.testing.util.ResourceUtils.resourceAsString;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.equalToCompressingWhiteSpace;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class EmailHtmlAndTextTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void multiTemplateContent() throws MessagingException, javax.mail.MessagingException, IOException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Multi")
                .body().template("template/mixed/simple", new SimpleBean("bar", 42))
                .to("recipient@sii.fr"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("Multi"))
        .from().address(hasItems("test.sender@sii.fr")).and()
        .to().address(hasItems("recipient@sii.fr")).and()
        .body()                                                                                              (1)
          .contentAsString(isIdenticalHtml(resourceAsString("/expected/simple_bar_42.html")))              (2)
          .contentType(startsWith("text/html")).and()                                                      (3)
        .alternative()                                                                                       (4)
          .contentAsString(equalToCompressingWhiteSpace(resourceAsString("/expected/simple_bar_42.txt")))  (5)
          .contentType(startsWith("text/plain")).and()                                                     (6)
        .attachments(emptyIterable());
    // @formatter:on
  }

  public static class SimpleBean {
    private String name;
    private int value;
    public SimpleBean(String name, int value) {
      super();
      this.name = name;
      this.value = value;
    }
    public String getName() {
      return name;
    }
    public int getValue() {
      return value;
    }
  }
}
1 Access to main body assertions
2 Assert that main body message is HTML content and is similar as an expected HTML content loaded from classpath
3 Assert that main body message mimetype is text/html
4 Access to alternative assertions
5 Assert that alternative body message is text content and is exactly the expected text content loaded from classpath
6 Assert that alternative body message mimetype is text/plain
thymeleaf HTML template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title" th:text="${name}"></h1>
        <p class="text" th:text="${value}"></p>
    </body>
</html>
freemarker logo Text template
${name} ${value}
html Expected HTML
<!DOCTYPE html>

<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1 class="title">bar</h1>
        <p class="text">42</p>
    </body>
</html>
txt Expected text

4.2.5. Testing attachments

You can also test that attachments are sent correctly.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static fr.sii.ogham.testing.assertion.util.EmailUtils.ATTACHMENT_DISPOSITION;
import static fr.sii.ogham.testing.util.ResourceUtils.resource;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class EmailAttachmentTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void simple() throws MessagingException, javax.mail.MessagingException, IOException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Test")
                .body().string("body")
                .to("recipient@sii.fr")
                .attach().resource("/attachment/test.pdf"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("Test"))
        .from().address(hasItems("test.sender@sii.fr")).and()
        .to().address(hasItems("recipient@sii.fr")).and()
        .body()
          .contentAsString(is("body"))
          .contentType(startsWith("text/plain")).and()
        .alternative(nullValue())
        .attachments(hasSize(1))                              (1)
        .attachment(0)                                        (2)
          .content(is(resource("/attachment/test.pdf")))    (3)
          .contentType(startsWith("application/pdf"))       (4)
          .filename(is("test.pdf"))                         (5)
          .disposition(is(ATTACHMENT_DISPOSITION));         (6)
    // @formatter:on
  }
}
1 Assert that exactly one attachment is attached to the received email
2 Access the first attachment for defining assertions on it
3 Assert that the first received file is exactly the same as the sent one
4 Assert that the detected mimetype of the first attachment is correct
5 Assert that the name of the first attachment is the expected one
6 Assert that the attachment disposition is correct

4.2.6. Testing several emails at once

When you send an email to several recipients, there is one message per recipient. So if you send an email to 6 recipients, you may need to ensure that the 6 messages are received correctly. Ogham provides the every method to apply defined assertions on all messages.

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class SeveralRecipientsTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void severalRecipients() throws MessagingException, javax.mail.MessagingException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Simple")
                .body().string("string body")
                .to("recipient1@sii.fr", "recipient2@sii.fr", "recipient3@sii.fr")
                .cc("recipient4@sii.fr", "recipient5@sii.fr")
                .bcc("recipient6@sii.fr"));
    assertThat(greenMail).receivedMessages()
      .count(is(6))                                                               (1)
      .every()                                                                    (2)
        .subject(is("Simple"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .to()
          .address(containsInAnyOrder("recipient1@sii.fr",                    (3)
                        "recipient2@sii.fr",
                        "recipient3@sii.fr")).and()
        .cc()
          .address(containsInAnyOrder("recipient4@sii.fr",                    (4)
                        "recipient5@sii.fr")).and()
        .body()
          .contentAsString(is("string body"))
          .contentType(startsWith("text/plain")).and()
        .alternative(nullValue())
        .attachments(emptyIterable());
    // @formatter:on
  }
}
1 Assert that 6 distinct messages are received
2 every applies all later defined assertions to all messages (the 6 messages)
3 Assert that each received messages has exactly 3 to recipients
4 Assert that each received messages has exactly 2 cc recipients

The bcc recipient is not testable. Indeed, bcc recipients are hidden recipients that must not be visible. So the email is received (there are 6 received messages not 5). But bcc field of each email are empty because they are not present in the sent email. The bcc field is just used for routing.

Sometimes you also need to send several different emails. The emails may have some identical information (sender or subject for example). So you can mix every (define assertions all messages) with message (define assertion for one particular message).

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class SeveralEmailsTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void severalDisctinctMessages() throws MessagingException, javax.mail.MessagingException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Simple")
                .body().string("string body 1")
                .to("recipient1@sii.fr"));
    oghamService.send(new Email()
                .subject("Simple")
                .body().string("string body 2")
                .to("recipient2@sii.fr"));
    oghamService.send(new Email()
                .subject("Simple")
                .body().string("string body 3")
                .to("recipient3@sii.fr"));
    assertThat(greenMail).receivedMessages()
      .count(is(3))
      .every()                                                   (1)
        .subject(is("Simple"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .body()
          .contentType(startsWith("text/plain")).and()
        .alternative(nullValue())
        .attachments(emptyIterable())
        .and()
      .message(0)                                                (2)
        .body()
          .contentAsString(is("string body 1"))
          .and()
        .to()
          .address(hasItems("recipient1@sii.fr"))
          .and()
        .and()
      .message(1)                                                (3)
        .body()
          .contentAsString(is("string body 2"))
          .and()
        .to()
          .address(hasItems("recipient2@sii.fr"))
          .and()
        .and()
      .message(2)                                                (4)
        .body()
          .contentAsString(is("string body 3"))
          .and()
        .to()
          .address(hasItems("recipient3@sii.fr"));
    // @formatter:on
  }
}
1 Shared assertions (subject, sender and body mimetype)
2 Specific assertions for first sent message (recipient and message content)
3 Specific assertions for second sent message (recipient and message content)
4 Specific assertions for third sent message (recipient and message content)

4.2.7. Testing with SMTP authentication

GreenMail allows to start the SMTP server with user and credentials.

java logo Java
package oghamall.it.email;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.testing.extension.junit.LoggingTestRule;
import fr.sii.ogham.testing.extension.junit.email.RandomPortGreenMailRule;

public class EmailSMTPAuthenticationTest {
  private MessagingService oghamService;

  @Rule
  public final LoggingTestRule loggingRule = new LoggingTestRule();

  @Rule
  public final GreenMailRule greenMail = new RandomPortGreenMailRule();

  @Before
  public void setUp() throws IOException {
    greenMail.setUser("test.sender@sii.fr", "test.sender", "password");             (1)
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties("/application.properties")
          .properties()
            .set("mail.smtp.host", greenMail.getSmtp().getBindTo())
            .set("mail.smtp.port", greenMail.getSmtp().getPort())
            .set("mail.smtp.auth", "true")                        (2)
            .set("ogham.email.javamail.authenticator.username", "test.sender")      (3)
            .set("ogham.email.javamail.authenticator.password", "password")       (4)
            .and()
          .and()
        .build();
  }

  @Test
  public void authenticated() throws MessagingException, javax.mail.MessagingException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Simple")
                .content("string body")
                .to("Recipient Name <recipient@sii.fr>"));
    assertThat(greenMail).receivedMessages()
      .count(is(1))
      .message(0)
        .subject(is("Simple"))
        .from()
          .address(hasItems("test.sender@sii.fr"))
          .personal(hasItems("Sender Name")).and()
        .to()
          .address(hasItems("recipient@sii.fr"))
          .personal(hasItems("Recipient Name")).and()
        .body()
          .contentAsString(is("string body"))
          .contentType(startsWith("text/plain")).and()
        .alternative(nullValue())
        .attachments(emptyIterable());
    // @formatter:on
  }
}
1 Configure GreenMail to register a user
2 Configure JavaMail to enable authentication
3 Configure Ogham to provide the authentication username
4 Configure Ogham to provide the authentication password
Only the setup differs, the test is the same.

4.2.8. Testing with SendGrid

4.3. Testing SMS

4.3.1. First test

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 jSMPP as local SMPP server.

java logo Java
package fr.sii.ogham.sample.test;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.is;

import java.io.IOException;

import org.jsmpp.bean.SubmitSm;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;
import fr.sii.ogham.testing.extension.junit.sms.JsmppServerRule;
import fr.sii.ogham.testing.extension.junit.sms.SmppServerRule;

public class SmsTestSample {
  private MessagingService oghamService;

  @Rule
  public final SmppServerRule<SubmitSm> 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

4.3.2. Testing long messages and several recipients

When a long message is sent, it must be split. Each part of the message is received by the SMPP server as distinct messages. The first message doesn’t have any additional information indicating that it is split. Successive messages have an additional header (contained in the body). Ogham hides this protocol specific complexity. So you can test your SMS without needing to know about the header. Ogham provides message method to access a particular message for doing assertions on it. Ogham also provides every to apply same assertions on all received messages.

Sending a SMS to several recipients is the same as sending several distinct SMS. So the SMPP server receives the same number as SMS as the number of recipients. In that case, you can use every to make same assertions on all messages.

java logo Java
package fr.sii.ogham.sample.test;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.is;

import java.io.IOException;

import org.jsmpp.bean.SubmitSm;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;
import fr.sii.ogham.testing.extension.junit.sms.JsmppServerRule;
import fr.sii.ogham.testing.extension.junit.sms.SmppServerRule;

public class LongSmsTestSample {
  private MessagingService oghamService;

  @Rule
  public final SmppServerRule<SubmitSm> smppServer = new JsmppServerRule();

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.sms.from.default-value", "+33603040506")
            .set("ogham.sms.smpp.host", "localhost")
            .set("ogham.sms.smpp.port", String.valueOf(smppServer.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void longMessageUsingGsm8bit() throws MessagingException, IOException {
    // @formatter:off
    oghamService.send(new Sms()
              .message().string("sms content with a very very very loooooooooo"
                  + "oooooooooonnnnnnnnnnnnnnnnng message that is"
                  + " over 140 characters in order to test the be"
                  + "havior of the sender when message has to be split")
              .to("0601020304"));
    assertThat(smppServer).receivedMessages()
      .count(is(2))                                                                   (1)
      .message(0)                                                                     (2)
        .content(is("sms content with a very very very looooooooooooooooooo"        (3)
            + "onnnnnnnnnnnnnnnnng message that is over 140 characters "
            + "in order to test the beh")).and()
      .message(1)                                                                     (4)
        .content(is("avior of the sender when message has to be split")).and()      (5)
      .every()                                                                        (6)
        .from()
          .number(is("+33603040506"))                                             (7)
          .and()
        .to()
          .number(is("0601020304"));                                              (8)
    // @formatter:on
  }
}
1 Message is split so the SMPP server receives two messages
2 Access first message to declare assertions on it
3 Assert that the first message is correctly split (contains the beginning of the message)
4 Access second message to declare assertions on it
5 Assert that the second message contains the end of the whole message
6 every applies all later defined assertions to all messages (the 2 messages here)
7 Assert that the sender phone number is +33603040506 for every message
8 Assert that the recipient phone number is 0601020304 for every message
  • Simulate slow server using @SmppServerConfig

  • Explain type of number (explanation + why it is important + sample)

  • Explain numbering plan indicator (explanation + why it is important + sample)

  • Add sample to show how to test with ovh?

  • Should use same assertion API (see https://github.com/groupe-sii/ogham/issues/96)

4.4. Fail at end

Instead of failing immediately on first assertion, Ogham also provides a way to run all assertions without failing and report all failures at the end. Here is an example with email (it is exactly the same for SMS):

java logo Java
package fr.sii.ogham.sample.test;

import static com.icegreen.greenmail.util.ServerSetupTest.SMTP;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertAll;
import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static fr.sii.ogham.testing.assertion.OghamMatchers.isSimilarHtml;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class AssertAllEmailTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new GreenMailRule(SMTP);

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", SMTP.getBindAddress())
            .set("mail.smtp.port", String.valueOf(SMTP.getPort()))
            .and()
          .and()
        .build();
  }

  @Test
  public void simple() throws MessagingException, javax.mail.MessagingException {
    // @formatter:off
    oghamService.send(new Email()
                .subject("Simple (assertAll)")
                .body().string("<html><body>Hello world!</body></html>")
                .to("Recipient Name <recipient@sii.fr>"));
    assertAll(r ->                                                       (1)
      assertThat(greenMail, r).receivedMessages()                      (2)
        .count(is(1))
        .message(0)
          .subject(is("Simple (assertAll)"))
          .from()
            .address(hasItems("test.sender@sii.fr"))
            .personal(hasItems("Sender Name")).and()
          .to()
            .address(hasItems("recipient@sii.fr"))
            .personal(hasItems("Recipient Name")).and()
          .body()
            .contentAsString(isSimilarHtml("<html><body>Hello world!</body></html>"))
            .contentType(startsWith("text/html")).and()
          .alternative(nullValue())
          .attachments(emptyIterable())
    );
    // @formatter:on
  }
}
1 Wrap existing assertion with OghamAssertions.assertAll. The r parameter is then used to register failed assertions.
2 Pass the r parameter to existing OghamAssertions.asserThat.

When some assertions fail, Ogham also does its best to present failures in a way that developer can quickly understand. If we make the following changes to the previous sample:

  • Change subject (send with Simple (assertAll))

  • Change from address check (test.sender1@sii.fr instead of test.sender@sii.fr)

  • Change html body check (Goodbye instead of Hello world!)

Then the result is:

tests assertall junit trace
Figure 11. JUnit stacktrace view

Double-clicking on the error opens the comparison view:

tests assertall junit comparison
Figure 12. JUnit comparison view

That way, you can quickly compare HTML results.

Why HTML can be compared but not others

Hamcrest matchers throw an AssertionError when an assertion fails.

Ogham provides some additional matchers (like isSimilarHtml) that add additional information so a comparison can be easily done and a org.junit.ComparisonFailure is thrown instead (so JUnit eclipse plugin can handle it).

4.5. Random ports

4.5.1. Standalone tests

Ogham provides RandomPortGreenMailRule that simply extends GreenMailRule to start on a random port instead of a fixed port.

Example 9. Random SMTP port using GreenMail
java logo Java
package fr.sii.ogham.sample.test;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;

import java.io.IOException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.testing.extension.junit.email.RandomPortGreenMailRule;

public class RandomPortEmailTestSample {
  private MessagingService oghamService;

  @Rule
  public final GreenMailRule greenMail = new RandomPortGreenMailRule();        (1)

  @Before
  public void setUp() throws IOException {
    oghamService = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("ogham.email.from.default-value", "Sender Name <test.sender@sii.fr>")
            .set("mail.smtp.host", greenMail.getSmtp().getBindTo())  (2)
            .set("mail.smtp.port", greenMail.getSmtp().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 <recipient@sii.fr>"));
    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 on a random port
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

For SMS testing, you don’t have to change anything, it uses random port by default.

4.5.2. Spring Boot tests

Ogham provides Spring Boot extensions that can be used in tests to benefit from random ports easily.

Example 10. Random ports in Spring Boot tests
java logo Email (JUnit 5)
package fr.sii.ogham.sample.springboot.email;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;

import com.icegreen.greenmail.junit5.GreenMailExtension;

import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.testing.extension.spring.GreenMailInitializer;
import mock.MockApplication;

@SpringBootTest(classes = MockApplication.class, properties = {
  "mail.smtp.host=127.0.0.1",
  "mail.smtp.port=${greenmail.smtp.port}"                              (1)
})
@ContextConfiguration(initializers = GreenMailInitializer.class)         (2)
public class RandomSmtpPortJUnit5TestSample {
  @RegisterExtension @Autowired public GreenMailExtension greenMail;   (3)
  @Autowired MessagingService messagingService;

  @Test
  public void foo() throws Exception {
    // some code to test your application here
    // that sends email through SMTP
    messagingService.send(new Email()
        .subject("Random port")
        .from("foo@yopmail.com")
        .to("bar@yopmail.com")
        .body().string("Random port sample"));
    // make assertions
    assertThat(greenMail).receivedMessages()                         (4)
      .count(is(1));
  }
}
1 The property greenmail.smtp.port can be used in the tested application that uses Ogham.
2 Register GreenMailInitializer to automatically start GreenMail SMTP service on a random port and register GreenMailExtension bean in test context.
3 Use @Autowired to inject GreenMailExtension instance that uses the random port.
4 Make assertions on GreenMail as usual.
java logo SMS (JUnit 5)
package fr.sii.ogham.sample.springboot.sms;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.is;

import org.jsmpp.bean.SubmitSm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;

import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;
import fr.sii.ogham.testing.extension.junit.sms.SmppServerExtension;
import fr.sii.ogham.testing.extension.spring.JsmppServerInitializer;
import mock.MockApplication;

@SpringBootTest(classes = MockApplication.class, properties = {
  "ogham.sms.smpp.host=127.0.0.1",
  "ogham.sms.smpp.port=${jsmpp.server.port}"                                        (1)
})
@ContextConfiguration(initializers = JsmppServerInitializer.class)                    (2)
public class RandomSmppPortJUnit5TestSample {
  @RegisterExtension @Autowired public SmppServerExtension<SubmitSm> smppServer;    (3)
  @Autowired MessagingService messagingService;

  @Test
  public void foo() throws Exception {
    // some code to test your application here
    // that sends SMS through SMPP
    messagingService.send(new Sms()
        .from("0000000000")
        .to("1111111111")
        .message().string("Random port"));
    // make assertions
    assertThat(smppServer).receivedMessages()                                     (4)
      .count(is(1));
  }
}
1 The property jsmpp.server.port can be used in the tested application that uses Ogham.
2 Register JsmppServerInitializer to automatically start JSMPPServer on a random port and register JsmppServerExtension bean in test context.
3 Use @Autowired to inject JsmppServerExtension instance that uses the random port.
4 Make assertions on JSMPPServer as usual.
java logo Email (JUnit 4)
package fr.sii.ogham.sample.springboot.email;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.is;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.icegreen.greenmail.junit4.GreenMailRule;

import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.testing.extension.spring.GreenMailInitializer;
import mock.MockApplication;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MockApplication.class, properties = {
  "mail.smtp.host=127.0.0.1",
  "mail.smtp.port=${greenmail.smtp.port}"                              (1)
})
@ContextConfiguration(initializers = GreenMailInitializer.class)         (2)
public class RandomSmtpPortJUnit4TestSample {
  @Rule @Autowired public GreenMailRule greenMail;                     (3)
  @Autowired MessagingService messagingService;

  @Test
  public void foo() throws Exception {
    // some code to test your application here
    // that sends email through SMTP
    messagingService.send(new Email()
        .subject("Random port")
        .from("foo@yopmail.com")
        .to("bar@yopmail.com")
        .body().string("Random port sample"));
    // make assertions
    assertThat(greenMail).receivedMessages()                         (4)
      .count(is(1));
  }
}
1 The property greenmail.smtp.port can be used in the tested application that uses Ogham.
2 Register GreenMailInitializer to automatically start GreenMail SMTP service on a random port and register GreenMailRule bean in test context.
3 Use @Autowired to inject GreenMailRule instance that uses the random port.
4 Make assertions on GreenMail as usual.
java logo SMS (JUnit 4)
package fr.sii.ogham.sample.springboot.sms;

import static fr.sii.ogham.testing.assertion.OghamAssertions.assertThat;
import static org.hamcrest.Matchers.is;

import org.jsmpp.bean.SubmitSm;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;
import fr.sii.ogham.testing.extension.junit.sms.SmppServerRule;
import fr.sii.ogham.testing.extension.spring.JsmppServerInitializer;
import mock.MockApplication;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MockApplication.class, properties = {
  "ogham.sms.smpp.host=127.0.0.1",
  "ogham.sms.smpp.port=${jsmpp.server.port}"                                   (1)
})
@ContextConfiguration(initializers = JsmppServerInitializer.class)               (2)
public class RandomSmppPortJUnit4TestSample {
  @Rule @Autowired public SmppServerRule<SubmitSm> smppServer;                 (3)
  @Autowired MessagingService messagingService;

  @Test
  public void foo() throws Exception {
    // some code to test your application here
    // that sends SMS through SMPP
    messagingService.send(new Sms()
        .from("0000000000")
        .to("1111111111")
        .message().string("Random port"));
    // make assertions
    assertThat(smppServer).receivedMessages()                                (4)
      .count(is(1));
  }
}
1 The property jsmpp.server.port can be used in the tested application that uses Ogham.
2 Register JsmppServerInitializer to automatically start JSMPPServer on a random port and register JsmppServerRule bean in test context.
3 Use @Autowired to inject JsmppServerRule instance that uses the random port.
4 Make assertions on JSMPPServer as usual.

5. Tips

5.1. Automatic configuration

Ogham automatically adapts according to the presence or not of some classes in the classpath and values of some configuration properties. To do so, there are several concepts that have each a specific role and that interact together.

5.1.1. Concepts

5.1.1.1. MessagingService class

The MessagingService is the facade to send any kind of message. Developer should use only MessagingService.send() method. The actual work is then internally delegated to sender implementations and template engine integrations that is not visible by the developer. Even if internal implementations are not visible to the developer, Ogham behavior is fully adaptable thanks to Builder classes.

5.1.1.2. Builder classes

Ogham is fully configurable and extensible.

To adapt the behavior to the application needs, Ogham provides Builder classes. Their purpose is to separate the concerns:

  • Implementations are simple and do the actual work.

  • Builders create implementation instances according to the application needs.

Builders hide complexity by offering methods that simplifies the construction of the implementations.

This follows the builder design pattern principles.

5.1.1.3. MessagingConfigurer classes

A MessagingConfigurer is responsible to provide the best behavior by default.

Each MessagingConfigurer calls the Builder methods to configure implementations to provide a consistent behavior. They also register configuration properties that can be configured externally by the developer to adapt the default behavior.

Each MessagingConfigurer may be automatically found and registered by Ogham using @ConfigurerFor annotation. If the automatic class scanning is not enabled, each MessagingConfigurer can be registered manually.

MessagingConfigurer classes are central for the automatic configuration.

5.1.1.4. MessagingBuilder

The purpose of MessagingBuilder is to provide to the developer a single entrypoint to configure and get a MessagingService instance. MessagingBuilder can automatically scan classpath to find MessagingConfigurer classes and instantiate them.

MessagingBuilder provides static methods to ease MessagingBuilder creation and automatic registration:

  • MessagingBuilder.empty() methods: Instantiate a MessagingBuilder with no auto-configuration at all (no class scanning to discover MessagingConfigurer classes).

  • MessagingBuilder.minimal() methods: Instantiate a MessagingBuilder that runs classpath scanning to find MessagingConfigurer classes that are annotated with @ConfigurerFor annotation. Only MessagingConfigurer classes that target minimal static factory are instantiated. This is useful to benefit for general Ogham behavior and features (such as template engines, automatic mimetype detection, auto-filling of messages, …​). Sender implementations are not automatically registered yet.

  • MessagingBuilder.standard() methods: Instantiate a MessagingBuilder that runs classpath scanning to find MessagingConfigurer classes that are annotated with @ConfigurerFor annotation. Only MessagingConfigurer classes that target standard static factory are instantiated. All Ogham features are registered including all available sender implementations.

A MessagingConfigurer targets one or several MessagingBuilder static factory methods through the @ConfigurerFor annotation (using the targetedBuilder field).

5.1.2. Auto-configuration lifecycle and configuration phases

@ConfigurerFor has a field named phase() to indicate when the MessagingConfigurer must be executed. There are two phases: ConfigurationPhase.AFTER_INIT and ConfigurationPhase.BEFORE_BUILD.

Here is the auto-configuration lifecycle:

  1. MessagingBuilder instantiation (using MessagingBuilder.standard() or MessagingBuilder.minimal()).

  2. Search MessagingConfigurer classes in the classpath and register found classes ordered by higher priority.

  3. Trigger ConfigurationPhase.AFTER_INIT phase: Instantiate and configure previously registered MessagingConfigurer classes (only configurers registered for AFTER_INIT phase).

  4. Developer can configure Ogham using MessagingBuilder instance (created at step 1).

  5. Developer has finished configuring Ogham so he calls MessagingBuilder.build().

  6. Trigger ConfigurationPhase.BEFORE_BUILD phase: Instantiate and configure previously registered MessagingConfigurer classes (only configurers registered for BEFORE_BUILD phase).

  7. Instantiate MessagingService according to MessagingBuilder configuration.

  8. Developer gets an instance of MessagingService completely configured for his needs.

Early initialization phase and property evaluation

If an Ogham extension provides a MessagingConfigurer that is initialized during AFTER_INIT phase and this configurer needs to evaluate a property value to adapt the behavior, it may not work as expected.

Actually, the developer can configure value for properties at the step 4 while AFTER_INIT phase is triggered at step 3.

5.2. Automatic MimeType detection

  • Explain that Ogham can detect mimetype automatically

  • Explain that Ogham supports several implementations

  • Explain that Ogham can use Tika

  • Explain how to configure default mimetype

  • Explain how to forbid some mimetypes

  • Explain how to replace a mimetype by another

  • Add link to extend documentation to add custom mimetype detector

5.3. Automatic configuration for well-known service providers

  • Explain aim of service providers

  • List predefined service providers

  • List all error codes with the explanation and how to fix it (dedicated appendix ?)

  • Example: Gmail throws javax.mail.AuthenticationFailedException: 534-5.7.14

  • Add all configuration properties (dedicated appendix ?), their default values, their effect and a description

  • Add all Spring Boot properties supported by Ogham, support conditions and their effect

6. Advanced configuration

6.1. Auto re-send messages

6.1.1. Retry handling

Some messages may fail while sending (due to network error for example). Ogham provides a mechanism to automatically resend a message when it fails under some circumstances.

Ogham provides several strategies for retry management:

  1. fixedDelay(): Wait for a fixed delay between each attempt (wait for the end of the last execution before start waiting)

  2. fixedInterval(): Retry on fixed interval (do not wait for the end of the last execution)

  3. exponentialDelay(): Wait for a delay that will be doubled between each attempt (wait for the end of the last execution before start waiting)

  4. perExecutionDelay(): Use a specific delay for each execution (for example: 100ms after the first execution, 500ms after the second, 8000ms after the third, …​)

To enable one of these strategies, don’t change your code. You just need to update Ogham configuration.

Default configuration

Default Ogham configuration (using MessagingBuiler.standard() or MessagingBuilder.minimal()) doesn’t enable automatic re-send of messages.

Skip retry for some errors

To prevent retrying for nothing, Ogham analyze the raised error (and associated causes) to skip retry. This happens if the exception (or cause) met one this condition:

  • If the error is a JVM error (instance of Error). This kind of errors should not be ignored.

  • If the preparation of the message has failed (template parsing, associated resource not found, mimetype detection failed, message is not valid, …​). This kind of error will always give the same result.

  • If the developer used an invalid value (null, illegal argument, …​). If the value is invalid, there is no point in retrying.

This is the default configuration for all kind of messages (email, SMS, …​). In addition to general exceptions, each sender implementation may register other conditions (like Cloudhopper for example that skip exceptions for SMS preparation).

Add custom rule to skip retry

You may need to skip retry for some other exceptions. Ogham lets you register custom conditions.

Several strategies configured

Even if several strategies are configured, only one can be used. Therefore, Ogham defines the following priority for the strategies (uses the first one that is fully configured):

  • perExecutionDelay()

  • exponentialDelay()

  • fixedInterval()

  • fixedDelay()

Using interval

When using a strategy based on interval (not waiting for the end of the last execution), there may have unexpected behaviors: the current execution may not be finished while a new retry is started. This may lead to several messages sent with the same content for example (in case the execution succeeds but take longer that configured interval).

6.1.1.1. Fixed delay

It retries several times with a fixed delay to wait after the last execution failure until the maximum attempts is reached.

Example 11. How it works
Message sent

This diagram shows what happens if the message can’t be sent twice but can be sent the third time. The delay is configured to 5 seconds.

For the explanation, the server returns a response after 10 seconds (timeout).

diag a06f51132cf94cbf725435783306e458
Message can’t be sent

This diagram shows what happens if the message can’t be sent and the maximum attempts is reached. The delay is configured to 5 seconds. The maximum attempts is configured to 4.

For the explanation, the server returns a response after 10 seconds (timeout).

diag 147807c30d86cdff96b0084161d8d76a
Example 12. Enable re-send of email with fixed delay
java logo Java
package fr.sii.ogham.sample.standard.email;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class BasicClasspathPropertiesSample {
  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("classpath:email.properties")      (1)
          .and()
        .build();
    // send the email using fluent API
    service.send(new Email()
            .subject("BasicClasspathPropertiesSample")
            .body().string("email content")
            .to("ogham-test@yopmail.com"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
mail.smtp.host=<your server host>
mail.smtp.port=<your server port>
ogham.email.from.default-value=<email address to display for the sender user>
ogham.email.send-retry.max-attempts=10                 (1)
ogham.email.send-retry.delay-between-attempts=5000     (2)
1 Set the maximum attempts to 10
2 Set the delay (5 seconds) to wait for executing the next attempt after the execution failure
Example 13. Enable re-send of SMS with fixed delay
java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSampleExternalProperties {

  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    // Load properties from file that is in classpath (src/main/resources/sms.properties)
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sms.properties")                 (1)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()
            .message().string("sms content")
            .to("+33752962193"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
ogham.sms.smpp.host=<your server host>
ogham.sms.smpp.port=<your server port>
ogham.sms.smpp.system-id=<your server system ID>
ogham.sms.smpp.password=<your server password>
ogham.sms.from.default-value=<phone number to display for the sender>
ogham.sms.send-retry.max-attempts=10                 (1)
ogham.sms.send-retry.delay-between-attempts=5000     (2)
1 Set the maximum attempts to 10
2 Set the delay (5 seconds) to wait for executing the next attempt after the execution failure
6.1.1.2. Exponential delay

Retry several times with an initial delay to wait after the last execution failure. The following delays are doubled until the maximum attempts is reached.

Example 14. How it works
Message sent

This diagram shows what happens if the message can’t be sent twice but can be sent the third time. The initial delay is configured to 5 seconds.

For the explanation, the server returns a response after 10 seconds (timeout).

diag 1081b15e5b4d6c663f43a3c231b3036c
Message can’t be sent

This diagram shows what happens if the message can’t be sent and the maximum attempts is reached. The initial delay is configured to 5 seconds. The maximum attempts is configured to 4.

For the explanation, the server returns a response after 10 seconds (timeout).

diag 54e47c9bc8234ed61ccf076d78778170
Example 15. Enable re-send of email with exponential delay
java logo Java
package fr.sii.ogham.sample.standard.email;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class BasicClasspathPropertiesSample {
  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("classpath:email.properties")      (1)
          .and()
        .build();
    // send the email using fluent API
    service.send(new Email()
            .subject("BasicClasspathPropertiesSample")
            .body().string("email content")
            .to("ogham-test@yopmail.com"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
mail.smtp.host=<your server host>
mail.smtp.port=<your server port>
ogham.email.from.default-value=<email address to display for the sender user>
ogham.email.send-retry.max-attempts=10                    (1)
ogham.email.send-retry.exponential-initial-delay=5000     (2)
1 Set the maximum attempts to 10
2 Set the initial delay to 5 seconds. After each failing attempts, the delay is doubled
Example 16. Enable re-send of SMS with exponential delay
java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSampleExternalProperties {

  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    // Load properties from file that is in classpath (src/main/resources/sms.properties)
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sms.properties")                 (1)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()
            .message().string("sms content")
            .to("+33752962193"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
ogham.sms.smpp.host=<your server host>
ogham.sms.smpp.port=<your server port>
ogham.sms.smpp.system-id=<your server system ID>
ogham.sms.smpp.password=<your server password>
ogham.sms.from.default-value=<phone number to display for the sender>
ogham.sms.send-retry.max-attempts=10                    (1)
ogham.sms.send-retry.exponential-initial-delay=5000     (2)
1 Set the maximum attempts to 10
2 Set the initial delay to 5 seconds. After each failing attempts, the delay is doubled
6.1.1.3. Per execution delay

Retry several times with a fixed delay to wait after the last execution failure until the maximum attempts is reached. A specific delay is used between each execution.

More attempts than configured delays

If there are more attempts than the configured delays, the last delay is used for each remaining attempts.

Example 17. How it works
Message sent

This diagram shows what happens if the message can’t be sent twice but can be sent the third time. The configured delays are: 5 seconds and 25 seconds.

For the explanation, the server returns a response after 10 seconds (timeout).

diag e9cd46f72cc85592656b913b1391eb70
Message can’t be sent

This diagram shows what happens if the message can’t be sent and the maximum attempts is reached. The maximum attempts is configured to 4. The configured delays are: 5 seconds, 25 seconds and 32 seconds.

For the explanation, the server returns a response after 10 seconds (timeout).

diag 5f16fae08764dcaeaf866c6a040922d1
Example 18. Enable re-send of email with per excecution delay
java logo Java
package fr.sii.ogham.sample.standard.email;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class BasicClasspathPropertiesSample {
  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("classpath:email.properties")      (1)
          .and()
        .build();
    // send the email using fluent API
    service.send(new Email()
            .subject("BasicClasspathPropertiesSample")
            .body().string("email content")
            .to("ogham-test@yopmail.com"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
mail.smtp.host=<your server host>
mail.smtp.port=<your server port>
ogham.email.from.default-value=<email address to display for the sender user>
ogham.email.send-retry.max-attempts=10                           (1)
ogham.email.send-retry.per-execution-delays=5000,25000,32000     (2)
1 Set the maximum attempts to 10
2 Configure the delays: 5 seconds after first failure, 25 seconds after second failure and 32 seconds after third execution
Example 19. Enable re-send of SMS with per execution delay
java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSampleExternalProperties {

  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    // Load properties from file that is in classpath (src/main/resources/sms.properties)
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sms.properties")                 (1)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()
            .message().string("sms content")
            .to("+33752962193"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
ogham.sms.smpp.host=<your server host>
ogham.sms.smpp.port=<your server port>
ogham.sms.smpp.system-id=<your server system ID>
ogham.sms.smpp.password=<your server password>
ogham.sms.from.default-value=<phone number to display for the sender>
ogham.sms.send-retry.max-attempts=10                           (1)
ogham.sms.send-retry.per-execution-delays=5000,25000,32000     (2)
1 Set the maximum attempts to 10
2 Configure the delays: 5 seconds after first failure, 25 seconds after second failure and 32 seconds after third execution
6.1.1.4. Fixed interval

It retries several times with a fixed interval until the maximum attempts is reached. It doesn’t wait for the end of the previous execution.

Example 20. How it works
Message sent

This diagram shows what happens if the message can’t be sent twice but can be sent the third time. The interval is configured to 5 seconds.

For the explanation, the server returns a response after 3 seconds (timeout).

diag f931fe62484350584168ca1628d85119
Message can’t be sent

This diagram shows what happens if the message can’t be sent and the maximum attempts is reached. The delay is configured to 5 seconds. The maximum attempts is configured to 4.

For the explanation, the server returns a response after 10 seconds (timeout).

diag a8c3dd7e1465f4fa21dc72fdc61d96f4
Small interval

This diagram shows what happens if the message can’t be sent and the execution takes longer than the configured interval. The delay is configured to 4 seconds. The maximum attempts is configured to 4.

For the explanation, the server returns a response after 10 seconds (timeout).

The following diagram is theoretical. It totally depends on RetryExecutor implementation. The strategy just indicates when the next retry should happen in theory.

By default, SimpleRetryExecutor is used and everything runs in a single thread. So it waits for the end of the execution even if interval is elapsed. Therefore in this particular case, the interval may not be respected.

diag 1dc3c3c9a3dfcae0564a271efd08953d
Example 21. Enable re-send of email with fixed interval
java logo Java
package fr.sii.ogham.sample.standard.email;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;

public class BasicClasspathPropertiesSample {
  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("classpath:email.properties")      (1)
          .and()
        .build();
    // send the email using fluent API
    service.send(new Email()
            .subject("BasicClasspathPropertiesSample")
            .body().string("email content")
            .to("ogham-test@yopmail.com"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
mail.smtp.host=<your server host>
mail.smtp.port=<your server port>
ogham.email.from.default-value=<email address to display for the sender user>
ogham.email.send-retry.max-attempts=10                 (1)
ogham.email.send-retry.delay-between-attempts=5000     (2)
1 Set the maximum attempts to 10
2 Set the delay (5 seconds) to wait for executing the next attempt after the execution failure
Example 22. Enable re-send of SMS with fixed interval
java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSampleExternalProperties {

  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    // Load properties from file that is in classpath (src/main/resources/sms.properties)
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sms.properties")                 (1)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()
            .message().string("sms content")
            .to("+33752962193"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
ogham.sms.smpp.host=<your server host>
ogham.sms.smpp.port=<your server port>
ogham.sms.smpp.system-id=<your server system ID>
ogham.sms.smpp.password=<your server password>
ogham.sms.from.default-value=<phone number to display for the sender>
ogham.sms.send-retry.max-attempts=10                 (1)
ogham.sms.send-retry.delay-between-attempts=5000     (2)
1 Set the maximum attempts to 10
2 Set the delay (5 seconds) to wait for executing the next attempt after the execution failure

6.1.2. RetryExecutor

6.1.2.1. SimpleRetryExecutor

This is a simple implementation that runs in the current thread (no thread is created). It executes the action and if it fails, it suspends the current thread (using an Awaiter implementation) for some time (configured through RetryStrategy).

It repeats that until either the it succeeds or the maximum attempts is reached (or the strategy indicates that it should stop).

Currently Ogham only provides this implementation but other may come in future versions.

6.1.2.2. Custom executor
Example 23. Use custom RetryExecutor
java logo Java
package fr.sii.ogham.sample.standard.advanced;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.sii.ogham.core.async.Awaiter;
import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.exception.async.WaitException;
import fr.sii.ogham.core.exception.retry.MaximumAttemptsReachedException;
import fr.sii.ogham.core.exception.retry.RetryException;
import fr.sii.ogham.core.exception.retry.RetryExecutionInterruptedException;
import fr.sii.ogham.core.retry.RetryExecutor;
import fr.sii.ogham.core.retry.RetryStrategy;
import fr.sii.ogham.core.retry.RetryStrategyProvider;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.sms.message.Sms;

public class CustomRetryExecutorSample {
  private static final Logger LOG = LoggerFactory.getLogger(CustomRetryExecutorSample.class);

  private static final int INVALID_PORT = 65000;

  public static void main(String[] args) throws MessagingException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties()
            .set("mail.smtp.host", "localhost")
            .set("mail.smtp.port", INVALID_PORT)                                                              (1)
            .set("ogham.email.send-retry.max-attempts", 5)                                                    (2)
            .set("ogham.email.send-retry.delay-between-attempts", 500)                                        (3)
            .set("ogham.sms.smpp.host", "localhost")
            .set("ogham.sms.smpp.port", INVALID_PORT)                                                         (4)
            .set("ogham.sms.send-retry.max-attempts", 5)                                                      (5)
            .set("ogham.sms.send-retry.exponential-initial-delay", 500)                                       (6)
            .and()
          .and()
        .email()
          .autoRetry()
            .executor(CustomRetryExecutor::new)                                                               (7)
            .and()
          .and()
        .sms()
          .autoRetry()
            .executor(CustomRetryExecutor::new)                                                               (8)
            .and()
          .and()
        .build();
    try {
      // send an email using fluent API
      service.send(new Email()
              .subject("BasicSample")
              .body().string("email content")
              .from("sender@yopmail.com")
              .to("ogham-test@yopmail.com"));
    } catch(MessagingException e) {
      LOG.error("Failed to send email", e);
    }
    try {
      // send a sms using fluent API
      service.send(new Sms()
              .message().string("SMS content")
              .from("000000000")
              .to("111111111"));
    } catch(MessagingException e) {
      LOG.error("Failed to send SMS", e);
    }
  }

  /**
   * Custom strategy implementation. For the sample, the executor reproduces
   * the base behavior of SimpleRetryExecutor.
   *
   * @author Aurélien Baudet
   *
   */
  public static class CustomRetryExecutor implements RetryExecutor {
    private final RetryStrategyProvider retryStrategyProvider;
    private final Awaiter awaiter;

    public CustomRetryExecutor(RetryStrategyProvider retryStrategyProvider, Awaiter awaiter) {
      super();
      this.retryStrategyProvider = retryStrategyProvider;
      this.awaiter = awaiter;
    }

    @Override
    public <V> V execute(Callable<V> actionToRetry) throws RetryException {
      try {
        RetryStrategy retry = retryStrategyProvider.provide();                                                    (9)
        List<Exception> failures = new ArrayList<>();
        do {
          Instant executionStart = Instant.now();                                                               (10)
          try {
            return actionToRetry.call();                                                                      (11)
          } catch (Exception e) {
            failures.add(e);
            Instant next = retry.nextDate(executionStart, Instant.now());                                     (12)
            LOG.warn("Failed to execute action. Cause: {}.\nRetrying at {}", e.getMessage(), next, e);
            awaiter.waitUntil(next);                                                                          (13)
          }
        } while (!retry.terminated());                                                                            (14)
        throw new MaximumAttemptsReachedException("Maximum attempts to execute action is reached", failures);     (15)
      } catch (WaitException e) {
        throw new RetryExecutionInterruptedException(e);                                                          (16)
      }

    }

  }
}
1 Use an invalid SMTP port in order to test retry
2 Configure maximum attempts for email
3 Enable retry using a fixed delay between each attempts for email
4 Use an invalid SMPP port in order to test retry
5 Configure maximum attempts for SMS
6 Enable retry using a delay that is doubled between each attempts for SMS
7 Register custom RetryExecutor for email
8 Register custom RetryExecutor for SMS
9 Get the instance of the RetryStrategy that will compute the date of the next attempt
10 Date when the execution started
11 Execute the action (here it is sending an email or a SMS)
12 Ask RetryStrategy when should next attempt be executed
13 Ask Awaiter to wait until next date
14 Ask RetryStrategy if maximum attempts are reached or not
15 All executions have failed and maximum attempts are reached so throw MaximumAttemptsReachedException
16 If waiting has failed, wrap the exception

As Cloudhopper SMPP implementation (for sending SMS) also uses a RetryStrategy for connecting to SMPP server (10 maximum attempts spaced 5 seconds apart), you may notice in logs that connection is retried 50 times (5 times for sending multiplied by 10 connection attempts).

6.1.3. Skip custom exceptions

  • Explain how SimpleRetryExecutor works and limitations

  • Explain how to use custom executor

6.2. Email

6.2.1. Content-ID generator

  • Explain how to add custom CID generation

  • Explain how to register it

6.2.2. Catalog of SMTP properties

  • Add whole list of email properties

6.3. SMS

6.3.1. Message encoding

  • Describe SMPP protocol encodings and fields (like Data Coding Scheme)

  • Explain Ogham automatic guessing

  • Explain how to adapt automatic guessing

  • Explain how to disable automatic guessing and use a fixed encoding

  • Explain the impact on the splitting of messages

  • Note to indicate how to add custom encoding

6.3.2. Session management

Session (connection between the client and the server) may be handled in many ways. Ogham natively provides three strategies:

  • Always use a new session for each message

  • Reuse same session for several messages until the connection is lost

  • Actively keep the session alive

This section explains the difference between each implementation, how it works and how to use it.

6.3.2.1. Always use a new session

This strategy is the simplest one. It is useful when your application only sends some SMS sometimes. For each SMS, a session is created, the message is sent and the session is closed.

This is the default behavior.

The diagram explains how it works:

diag 7ddead08cf6b3809422d41f55e6d7895
Figure 13. How it works
6.3.2.2. Reuse session

This strategy opens a session when needed and try to reuse it if possible.

When sending the first message, a new session is created. Later, when sending the next message, if the session is still alive, this session is reused. As the connection is not actively maintained, the session may be killed by the server. Therefore to check if the session is still alive, an enquire_link request is sent. If a response is received from the server, then the session is still alive and the message can be sent using the same session. If a failure response or no response is received after some time from the server, then a new session must be created.

To check if the session is still alive, the enquire_link request is sent just before sending the real message. In order to prevent sending an enquire_link request before every message, the date of the last sent message or enquire_link is kept. This date is compared to a delay to ensure that no enquire_link is sent during this delay.

To enable reuse session strategy, update the configuration like this:

Example 24. Enable reuse session
java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSampleExternalProperties {

  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    // Load properties from file that is in classpath (src/main/resources/sms.properties)
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sms.properties")                 (1)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()
            .message().string("sms content")
            .to("+33752962193"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
ogham.sms.smpp.host=<your server host>
ogham.sms.smpp.port=<your server port>
ogham.sms.smpp.system-id=<your server system ID>
ogham.sms.smpp.password=<your server password>
ogham.sms.from.default-value=<phone number to display for the sender>
ogham.sms.cloudhopper.session.reuse-session.enable=true           (1)
1 Enable reuse session (with default values)

The following diagram explains how it works:

diag 4644cd346f4e29f19da7b4c4a46244a6
Figure 14. How reuse session works

By default, Ogham considers that session should remain opened if last interaction (connection, enquire_link or submit_sm) with the server happened less than 30 seconds ago (recommended value). If the server doesn’t send a response after a delay (10 seconds by default), Ogham will consider the connection as down.

Timings are configurable:

Example 25. Custom reuse session configuration
java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSampleExternalProperties {

  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    // Load properties from file that is in classpath (src/main/resources/sms.properties)
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sms.properties")                 (1)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()
            .message().string("sms content")
            .to("+33752962193"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
ogham.sms.smpp.host=<your server host>
ogham.sms.smpp.port=<your server port>
ogham.sms.smpp.system-id=<your server system ID>
ogham.sms.smpp.password=<your server password>
ogham.sms.from.default-value=<phone number to display for the sender>
ogham.sms.cloudhopper.session.reuse-session.enable=true
ogham.sms.cloudhopper.session.reuse-session.last-interaction-expiration-delay=60000       (1)
ogham.sms.cloudhopper.session.reuse-session.enquire-link-timeout=5000                     (2)
1 Change delay to consider last interaction enquire_link requests to 1 minute.
2 Change default response timeout to 5 seconds.
Connection lost

If the connection to the server is lost in the following cases:

  • The server has explicitly closed the session

  • The server has been shutdown

  • The server has been restarted

  • The network is not available

Ogham will detect that the session is down and a new session will be used for next message.

Expiration delay

If ogham.sms.cloudhopper.session.reuse-session.last-interaction-expiration-delay is too high, Ogham may consider that the session is still alive whereas the server may ignore the session (some servers may consider the client as outdated but without sending explicit close). In this case, no enquire_link request is sent. The message is sent using the same session. However, it will fail because Ogham reuses a session that is closed. In this case, the SMS can’t be sent and an error is raised.

It is up to the developer to configure the right values according to the server when using this strategy.

6.3.2.3. Keep session alive

You may need to keep the session opened in order to avoid opening and closing session too often. This may be necessary in situation where you send many SMS over the time.

To keep the session alive, Ogham will actively send enquire_link requests to the server to indicate that the client is still alive and ready to send SMS.

To enable keep alive strategy, just update the configuration:

Example 26. Enable keep alive
java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSampleExternalProperties {

  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    // Load properties from file that is in classpath (src/main/resources/sms.properties)
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sms.properties")                 (1)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()
            .message().string("sms content")
            .to("+33752962193"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
ogham.sms.smpp.host=<your server host>
ogham.sms.smpp.port=<your server port>
ogham.sms.smpp.system-id=<your server system ID>
ogham.sms.smpp.password=<your server password>
ogham.sms.from.default-value=<phone number to display for the sender>
ogham.sms.cloudhopper.session.keep-alive.enable=true           (1)
1 Enable active keep alive (with default values)

The following diagram explains how it works:

diag 9b8bc5f590eff6826bd7312d098259ca
Figure 15. How keep alive works

By default, Ogham will send enquire_link requests every 30 seconds (recommended value). The enquire_link requests will start after the first message is sent. If the server doesn’t send a response after a delay (10 seconds by default) 3 times in a row, Ogham will consider the connection as down.

Every value is configurable to adapt to the remote server:

Example 27. Custom keep alive configuration
java logo Java
package fr.sii.ogham.sample.standard.sms;

import java.io.IOException;

import fr.sii.ogham.core.builder.MessagingBuilder;
import fr.sii.ogham.core.exception.MessagingException;
import fr.sii.ogham.core.service.MessagingService;
import fr.sii.ogham.sms.message.Sms;

public class BasicSampleExternalProperties {

  public static void main(String[] args) throws MessagingException, IOException {
    // Instantiate the messaging service using default behavior and
    // provided properties
    // Load properties from file that is in classpath (src/main/resources/sms.properties)
    MessagingService service = MessagingBuilder.standard()
        .environment()
          .properties("/sms.properties")                 (1)
          .and()
        .build();
    // send the sms using fluent API
    service.send(new Sms()
            .message().string("sms content")
            .to("+33752962193"));
  }

}
1 Load properties from a file that is in the classpath.
properties Properties
ogham.sms.smpp.host=<your server host>
ogham.sms.smpp.port=<your server port>
ogham.sms.smpp.system-id=<your server system ID>
ogham.sms.smpp.password=<your server password>
ogham.sms.from.default-value=<phone number to display for the sender>
ogham.sms.cloudhopper.session.keep-alive.enable=true
ogham.sms.cloudhopper.session.keep-alive.connect-at-startup=true        (1)
ogham.sms.cloudhopper.session.keep-alive.enquire-link-interval=60000    (2)
ogham.sms.cloudhopper.session.keep-alive.enquire-link-timeout=5000      (3)
ogham.sms.cloudhopper.session.keep-alive.max-consecutive-timeouts=5     (4)
1 Directly start sending enquire_link requests to keep session alive when Ogham starts instead of waiting for first message.
2 Change default interval between enquire_link requests to 1 minute.
3 Change default response timeout to 5 seconds.
4 Change default number of consecutive enquire_link requests that end in timeout to 5.
Connection lost

There are many cases where the connection to the server may be lost:

  • The enquire_link request received no answer within the configured timeout

  • The server has been shutdown

  • The server has been restarted

  • The network is not available

  • …​

Ogham will detect that the session is outdated and that a new one is required. It will try to reconnect in background. If it succeeds, enquire_link requests will be sent again to actively maintain the new session. However, if it fails instead of trying to reconnect for nothing (the server may be shutdown for a long time), it doesn’t try to reconnect now. The reconnection attempt will be done when next message is about to be sent.

Moreover, reconnection process uses the same retry strategy as connection (10 attempts every 5 seconds by default).

6.3.3. Catalog of SMPP properties

SMPP is the standard protocol to send SMS. SMPP is quite complex and has many configurable properties.

There are configuration properties that are dependent of the sender implementation (for example Cloudhopper) and configuration properties that are shared between all implementations.

Table 1. Common SMPP properties
Property Description Required Default value

ogham.sms.smpp.host

The host of the SMPP or SMSC server (IP or address).

Yes

ogham.sms.smpp.port

The port of the SMPP or SMSC server

Yes

ogham.sms.smpp.system-id

The system_id parameter is used to identify an ESME ( External Short Message Entity) or an SMSC (Short Message Service Centre) at bind time. An ESME system_id identifies the ESME or ESME agent to the SMSC. The SMSC system_id provides an identification of the SMSC to the ESME.

Yes

ogham.sms.smpp.password

The password parameter is used by the SMSC to authenticate the identity of the binding ESME. The Service Provider may require ESME’s to provide a password when binding to the SMSC. This password is normally issued by the SMSC system administrator. The password parameter may also be used by the ESME to authenticate the identity of the binding SMSC (e.g. in the case of the outbind operation).

Yes

ogham.sms.smpp.bind-type

The bind command type: can be TRANSMITTER (send only), RECEIVER (to send messages only, not useful in Ogham context) or TRANSCEIVER (send and receive, may be used for acks).

No

TRANSMITTER

ogham.sms.smpp.system-type

The system_type parameter is used to categorize the type of ESME that is binding to the SMSC. Examples include “VMS” (voice mail system) and “OTA” (over-the-air activation system). Specification of the system_type is optional - some SMSC’s may not require ESME’s to provide this detail. In this case, the ESME can set the system_type to NULL. The system_type (optional) may be used to categorize the system, e.g., “EMAIL”, “WWW”, etc.

No

ogham.sms.smpp.user-data.use-short-message

Enable/disable use of short_message field to carry text message (named User Data).

No

true

ogham.sms.smpp.user-data.use-tlv-message-payload

Enable/disable use of message_payload optional TLV (Tag-Value-Length) parameter to carry text message (named User Data).

No

false

ogham.sms.smpp.split.enable

Enable/disable message splitting.

SMPP message (short_message) size is limited to 140 octets. Therefore long text messages has to be split into several segments and sent separately. The number of effective characters sent per segment depends on the character encoding (see how message split is handled).

No

true

ogham.sms.smpp.encoder.gsm7bit-packed.priority

Set priority for encoding text messages using GSM 7-bit encoding. GSM 7-bit encoding and GSM 8-bit encoding use the same character tables. Only 7 bits are necessary to represents characters. In GSM 8-bit encoding a leading 0 is added. However, GSM 7-bit encoding is packed. Every character is "merged" with the next one in order to use more characters for the same number of octets.

If priority value is 0 or negative, it disables GSM 7-bit encoding.

This encoding is disabled by default since it is not supported by many providers and leads to unexpected characters received by the end-user.

No

0 (disabled)

ogham.sms.smpp.encoder.gsm8bit.priority

Set priority for encoding text messages using GSM 8-bit encoding. GSM 7-bit encoding and GSM 8-bit encoding use the same character tables. Only 7 bits are necessary to represents characters. In GSM 8-bit encoding a leading 0 is added.

If priority value is 0 or negative, it disables GSM 8-bit encoding.

No

99000

ogham.sms.smpp.encoder.latin1.priority

Set priority for encoding text messages using Latin-1 (ISO-8859-1)

If priority value is 0 or negative, it disables Latin-1 encoding.

No

98000

ogham.sms.smpp.encoder.ucs2.priority

Set priority for encoding text messages using UCS-2. UCS-2 uses two octets per character.

If priority value is 0 or negative, it disables UCS-2 encoding.

No

90000

ogham.sms.smpp.encoder.auto-guess.enable

Enable/disable automatic guessing of message encoding.

If enabled, it automatically guess the best supported encoding in order to use the minimum octets:

  • It encodes using GSM 7-bit default alphabet if the message contains only characters defined in the table. Message is packed so the message can have a maximum length of 160 characters.

  • It encodes using GSM 8-bit data encoding if the message contains only characters that can be encoded on one octet and each character is present in the GSM 8-bit table.

  • It encodes using Latin 1 (ISO-8859-1) data encoding if the message contains only characters that can be encoded on one octet.

  • It encodes using UCS-2 encoding if the message contains special characters that can’t be encoded on one octet. Each character is encoded on two octets.

No

true

ogham.sms.smpp.encoder.default-charset

Set which Charset should be used if nothing else is configured.

No

GSM

ogham.sms.smpp.data-coding-scheme.auto.enable

Enable/disable automatic detection of Data Coding Scheme value according to the SMPP interface version.

Data Coding Scheme is a one-octet field in Short Messages (SM) and Cell Broadcast Messages (CB) which carries a basic information how the recipient handset should process the received message. The information includes:

  • the character set or message coding which determines the encoding of the message user data

  • the message class which determines to which component of the Mobile Station (MS) or User Equipment (UE) should be the message delivered

  • the request to automatically delete the message after reading

  • the state of flags indicating presence of unread voicemail, fax, e-mail or other messages

  • the indication that the message content is compressed

  • the language of the cell broadcast message

The field is described in 3GPP 23.040 and 3GPP 23.038 under the name TP-DCS (see SMS Data Coding Scheme).

SMPP 3.4 introduced a new list of data_coding values (see Short Message Peer to Peer).

Implementation that determines the Data Coding value is selected based on SMPP interface version if enabled.

No

true

ogham.sms.smpp.data-coding-scheme.value

Use the same Data Coding Scheme value for all messages. If set, disables automatic detection (see ogham.sms.smpp.data-coding-scheme.auto.enable)

No

Table 2. Cloudhopper specific SMPP properties

ogham.sms.cloudhopper.host

The host of the SMPP or SMSC server (IP or address).

This is an alias of ogham.sms.smpp.host but only used by Cloudhopper. If both are defined, this property takes precedence.

Yes

ogham.sms.cloudhopper.port

The port of the SMPP or SMSC server

This is an alias of ogham.sms.smpp.port but only used by Cloudhopper. If both are defined, this property takes precedence.

Yes

ogham.sms.cloudhopper.system-id

The system_id parameter is used to identify an ESME ( External Short Message Entity) or an SMSC (Short Message Service Centre) at bind time. An ESME system_id identifies the ESME or ESME agent to the SMSC. The SMSC system_id provides an identification of the SMSC to the ESME.

This is an alias of ogham.sms.smpp.system-id but only used by Cloudhopper. If both are defined, this property takes precedence.

Yes

ogham.sms.cloudhopper.password

The password parameter is used by the SMSC to authenticate the identity of the binding ESME. The Service Provider may require ESME’s to provide a password when binding to the SMSC. This password is normally issued by the SMSC system administrator. The password parameter may also be used by the ESME to authenticate the identity of the binding SMSC (e.g. in the case of the outbind operation).

This is an alias of ogham.sms.smpp.password but only used by Cloudhopper. If both are defined, this property takes precedence.

Yes

ogham.sms.cloudhopper.bind-type

The bind command type: can be TRANSMITTER (send only), RECEIVER (to send messages only, not useful in Ogham context) or TRANSCEIVER (send and receive, may be used for acks).

This is an alias of ogham.sms.smpp.bind-type but only used by Cloudhopper. If both are defined, this property takes precedence.

Yes

TRANSMITTER

ogham.sms.cloudhopper.system-type

The system_type parameter is used to categorize the type of ESME that is binding to the SMSC. Examples include “VMS” (voice mail system) and “OTA” (over-the-air activation system). Specification of the system_type is optional - some SMSC’s may not require ESME’s to provide this detail. In this case, the ESME can set the system_type to NULL. The system_type (optional) may be used to categorize the system, e.g., “EMAIL”, “WWW”, etc.

This is an alias of ogham.sms.smpp.system-type but only used by Cloudhopper. If both are defined, this property takes precedence.

Yes

ogham.sms.cloudhopper.interface-version

The SMPP protocol version (either 3.3, 3.4 or 5.0)

No

3.4 (supported by most providers)

ogham.sms.cloudhopper.user-data.use-short-message

Enable/disable use of short_message field to carry text message (named User Data).

This is an alias of ogham.sms.smpp.user-data.use-short-message but only used by Cloudhopper. If both are defined, this property takes precedence.

No

true

ogham.sms.cloudhopper.user-data.use-tlv-message-payload

Enable/disable use of message_payload optional TLV (Tag-Value-Length) parameter to carry text message (named User Data).

No

false

ogham.sms.cloudhopper.split.enable

Enable/disable message splitting.

SMPP message (short_message) size is limited to 140 octets. Therefore long text messages has to be split into several segments and sent separately. The number of effective characters sent per segment depends on the character encoding (see how message split is handled).

This is an alias of ogham.sms.smpp.split.enable but only used by Cloudhopper. If both are defined, this property takes precedence.

No

true

ogham.sms.cloudhopper.encoder.gsm7bit-packed.priority

Set priority for encoding text messages using GSM 7-bit encoding. GSM 7-bit encoding and GSM 8-bit encoding use the same character tables. Only 7 bits are necessary to represents characters. In GSM 8-bit encoding a leading 0 is added. However, GSM 7-bit encoding is packed. Every character is "merged" with the next one in order to use more characters for the same number of octets.

If priority value is 0 or negative, it disables GSM 7-bit encoding.

This encoding is disabled by default since it is not supported by many providers and leads to unexpected characters received by the end-user.

This is an alias of ogham.sms.smpp.encoder.gsm7bit-packed.priority but only used by Cloudhopper. If both are defined, this property takes precedence.

No

0 (disabled)

ogham.sms.cloudhopper.encoder.gsm8bit.priority

Set priority for encoding text messages using GSM 8-bit encoding. GSM 7-bit encoding and GSM 8-bit encoding use the same character tables. Only 7 bits are necessary to represents characters. In GSM 8-bit encoding a leading 0 is added.

If priority value is 0 or negative, it disables GSM 8-bit encoding.

This is an alias of ogham.sms.smpp.encoder.gsm8bit.priority but only used by Cloudhopper. If both are defined, this property takes precedence.

No

99000

ogham.sms.cloudhopper.encoder.latin1.priority

Set priority for encoding text messages using Latin-1 (ISO-8859-1)

If priority value is 0 or negative, it disables Latin-1 encoding.

This is an alias of ogham.sms.smpp.encoder.latin1.priority but only used by Cloudhopper. If both are defined, this property takes precedence.

No

98000

ogham.sms.cloudhopper.encoder.ucs2.priority

Set priority for encoding text messages using UCS-2. UCS-2 uses two octets per character.

If priority value is 0 or negative, it disables UCS-2 encoding.

This is an alias of ogham.sms.smpp.encoder.ucs2.priority but only used by Cloudhopper. If both are defined, this property takes precedence.

No

90000

ogham.sms.cloudhopper.encoder.auto-guess.enable

Enable/disable automatic guessing of message encoding.

If enabled, it automatically guess the best supported encoding in order to use the minimum octets:

  • It encodes using GSM 7-bit default alphabet if the message contains only characters defined in the table. Message is packed so the message can have a maximum length of 160 characters.

  • It encodes using GSM 8-bit data encoding if the message contains only characters that can be encoded on one octet and each character is present in the GSM 8-bit table.

  • It encodes using Latin 1 (ISO-8859-1) data encoding if the message contains only characters that can be encoded on one octet.

  • It encodes using UCS-2 encoding if the message contains special characters that can’t be encoded on one octet. Each character is encoded on two octets.

This is an alias of ogham.sms.smpp.encoder.auto-guess.enable but only used by Cloudhopper. If both are defined, this property takes precedence.

No

true

ogham.sms.cloudhopper.encoder.default-charset

Set which Charset should be used if nothing else is configured.

This is an alias of ogham.sms.smpp.encoder.default-charset but only used by Cloudhopper. If both are defined, this property takes precedence.

No

GSM

ogham.sms.cloudhopper.data-coding-scheme.auto.enable

Enable/disable automatic detection of Data Coding Scheme value according to the SMPP interface version.

Data Coding Scheme is a one-octet field in Short Messages (SM) and Cell Broadcast Messages (CB) which carries a basic information how the recipient handset should process the received message. The information includes:

  • the character set or message coding which determines the encoding of the message user data

  • the message class which determines to which component of the Mobile Station (MS) or User Equipment (UE) should be the message delivered

  • the request to automatically delete the message after reading

  • the state of flags indicating presence of unread voicemail, fax, e-mail or other messages

  • the indication that the message content is compressed

  • the language of the cell broadcast message

The field is described in 3GPP 23.040 and 3GPP 23.038 under the name TP-DCS (see SMS Data Coding Scheme).

SMPP 3.4 introduced a new list of data_coding values (see Short Message Peer to Peer).

Implementation that determines the Data Coding value is selected based on SMPP interface version if enabled.

This is an alias of ogham.sms.smpp.data-coding-scheme.auto.enable but only used by Cloudhopper. If both are defined, this property takes precedence.

No

true

ogham.sms.cloudhopper.data-coding-scheme.value

Use the same Data Coding Scheme value for all messages. If set, disables automatic detection (see ogham.sms.smpp.data-coding-scheme.auto.enable).

This is an alias of ogham.sms.smpp.data-coding-scheme.value but only used by Cloudhopper. If both are defined, this property takes precedence.

No

ogham.sms.cloudhopper.session.name

A name for the session (used to name threads).

No

ogham.sms.cloudhopper.session.bind-timeout

Set the maximum amount of time (in milliseconds) to wait for the success of a bind attempt to the SMSC.

No

5000 (5 seconds)

ogham.sms.cloudhopper.session.connect-timeout

Set the maximum amount of time (in milliseconds) to wait for a establishing the connection.

No

10000 (10 seconds)

ogham.sms.cloudhopper.session.request-expiry-timeout

Set the amount of time (milliseconds) to wait for an endpoint to respond to a request before it expires.

No

-1 (disabled)

ogham.sms.cloudhopper.session.window-monitor-interval

Sets the amount of time (milliseconds) between executions of monitoring the window for requests that expire. It’s recommended that this generally either matches or is half the value of ogham.sms.cloudhopper.session.request-expiry-timeout. Therefore, at worst a request would could take up 1.5X the ogham.sms.cloudhopper.session.request-expiry-timeout to clear out.

No

-1 (disabled)

ogham.sms.cloudhopper.session.window-size

Message exchange may be synchronous, where each peer waits for a response for each PDU being sent, or asynchronous, where multiple requests can be issued 7 without waiting and acknowledged in a skew order by the other peer; the number of unacknowledged requests is called a window

Sets the maximum number of requests permitted to be outstanding (unacknowledged) at a given time.

No

1

ogham.sms.cloudhopper.session.window-wait-timeout

Set the amount of time (milliseconds) to wait until a slot opens up in the send window.

No

60000 (1 minute)

ogham.sms.cloudhopper.session.write-timeout

Set the maximum amount of time (in milliseconds) to wait for bytes to be written when creating a new SMPP session

No

0 (no timeout, for backwards compatibility)

ogham.sms.cloudhopper.session.response-timeout

Set the maximum amount of time (in milliseconds) to wait until a valid response is received when a "submit" request is synchronously sends to the remote endpoint. The timeout value includes both waiting for a "window" slot, the time it takes to transmit the actual bytes on the socket, and for the remote endpoint to send a response back.

No

5000 (5 seconds)

ogham.sms.cloudhopper.session.unbind-timeout

Set the maximum amount of time (in milliseconds) to wait until the session is unbounded, waiting up to a specified period of milliseconds for an unbind response from the remote endpoint. Regardless of whether a proper unbind response was received, the socket/channel is closed.

No

5000 (5 seconds)

ogham.sms.cloudhopper.session.connect-retry.max-attempts

If the connection can’t be established then Ogham can retry several times until it reaches a maximum number of attempts.

Set this value to 0 to disable retry.

No

5 attempts

ogham.sms.cloudhopper.session.connect-retry.delay-between-attempts

Time to wait (in milliseconds) before retrying reconnection. The time to wait is a fixed value.

No

500

6.4. Use the builder to define your own behaviors

  • Explain what is a builder

  • Explain what is a configurer

  • Explain what is a service provider configurer

  • Explain relationship between builders and configurers

  • Explain phases ?

  • Explain how to add custom builder ? → link to extend.adoc

  • Explain how to add custom configurer ? → link to extend.adoc

6.4.1. Standard builder

  • Explain what is the "standard" builder

  • Explain what is registered

  • Should explain what is configured by each configurer ?

6.4.2. Minimal builder

  • Explain what is the "minimal" builder

  • Explain what is registered

  • Should explain what is configured by each configurer ?

6.4.3. Empty builder

  • Explain what is the "empty" builder

  • Explain what is registered

6.5. Intercept messages just before sending

  • Explain role of interceptors

  • Explain how to implement end register an interceptor

7. Extend

7.1. Custom email sender implementation

  • Explain how to implement custom sender (with sample)

  • Explain how to add a builder for the custom sender

  • Explain implementation selection

  • Explain conditions (based on classpath, properties, custom conditions)

  • Explain how to add a default configurer for the sender

7.2. Custom SMS sender implementation

  • Explain how to implement custom sender (with sample)

  • Explain how to add a builder for the custom sender

  • Explain implementation selection

  • Explain conditions (based on classpath, properties, custom conditions)

  • Explain how to add a default configurer for the sender

7.3. Use your custom template engine

  • Explain how to implement custom engine (with sample)

  • Explain template engine selection (detector)

  • Explain how to add a builder for the custom engine

  • Explain how to add custom engine detector

7.4. Create your own configurer

  • Explain how to implement a custom configurer

  • Explain how to register it (or how Ogham can find it automatically)

7.5. Create custom resource resolver

  • Explain how to register custom resource resolver

  • Explain how to associate with a lookup prefix

  • Explain how to handle template engine resolution (adapters)

  • Explain how to handle relative resources

7.6. Add new type of message

  • Explain how to add a new king of message

  • Explain design process

  • Explain how to add a builder into main builder

  • Explain how to contribute to Ogham code

Version: 3.1.0-SNAPSHOT.

Reflow Maven skin by devacfr.