Skip to content

Developer Guide

This section covers the data model, configuration reference, sending mechanics, and extension points available to developers integrating Email Manager into an application.

Data Model

Email Manager persists two core entities. An email holds the message metadata, recipient lists, and body. An attachment holds the binary content of a file associated with an email, and carries a foreign key back to the email it belongs to. Recipient lists (To, Cc, Bcc) are stored in dedicated collection tables to support multiple addresses per email.

---
config:
  layout: elk
---
erDiagram
    em_email {
        integer   email_id         PK
        varchar   sender_address
        varchar   subject
        text      body
        boolean   html
        varchar   status
        timestamp created_instant
        timestamp sent_instant
        text      failure_reason
    }
    em_attachment {
        integer   attachment_id    PK
        integer   email_id         FK
        varchar   file_name
        varchar   content_type
        timestamp added_instant
        blob      data
    }
    em_recipient_addresses {
        integer email_id           FK
        varchar recipient_address
    }
    em_carbon_copy_recipients {
        integer email_id               FK
        varchar carbon_copy_recipient
    }
    em_blind_carbon_copy_recipients {
        integer email_id                     FK
        varchar blind_carbon_copy_recipient
    }

    em_email ||--o{ em_attachment                   : "has"
    em_email ||--o{ em_recipient_addresses          : "sent to"
    em_email ||--o{ em_carbon_copy_recipients       : "cc'd to"
    em_email ||--o{ em_blind_carbon_copy_recipients : "bcc'd to"
Hold "Alt" / "Option" to enable pan & zoom

The html flag records whether the body must be delivered as HTML or as plain text. The failure_reason column holds the type and message of the last failed delivery attempt, and is cleared when a later attempt succeeds. It deliberately does not hold the full stack trace: a JavaMail trace carries the SMTP host, port, handshake detail and frequently the user name, and the failure reason is shown in the management view. The complete trace goes to the application log instead.

Database Schema

Email Manager manages five tables. The schema below represents the DDL generated by Hibernate for a standard relational database. Every table and column is named explicitly in the entity annotations, so the names below do not depend on the naming strategy in force.

CREATE SEQUENCE em_email_seq
    START WITH 1 INCREMENT BY 50;

CREATE SEQUENCE em_attachment_seq
    START WITH 1 INCREMENT BY 50;

CREATE TABLE em_email (
    email_id         INTEGER      NOT NULL,
    sender_address   VARCHAR(255) NOT NULL,
    subject          VARCHAR(255),
    body             TEXT,
    html             BOOLEAN      NOT NULL,
    status           VARCHAR(255),
    created_instant  TIMESTAMP(6) NOT NULL,
    sent_instant     TIMESTAMP(6),
    failure_reason   TEXT,
    CONSTRAINT pk_em_email PRIMARY KEY (email_id)
);

CREATE TABLE em_attachment (
    attachment_id  INTEGER NOT NULL,
    email_id       INTEGER,
    file_name      VARCHAR(255),
    content_type   VARCHAR(255),
    added_instant  TIMESTAMP(6),
    data           BLOB,
    CONSTRAINT pk_em_attachment PRIMARY KEY (attachment_id),
    CONSTRAINT fk_em_attachment_email
        FOREIGN KEY (email_id) REFERENCES em_email (email_id)
);

CREATE TABLE em_recipient_addresses (
    email_id           INTEGER NOT NULL,
    recipient_address  VARCHAR(255),
    CONSTRAINT fk_em_recipient_addresses_email
        FOREIGN KEY (email_id) REFERENCES em_email (email_id)
);

CREATE TABLE em_carbon_copy_recipients (
    email_id               INTEGER NOT NULL,
    carbon_copy_recipient  VARCHAR(255),
    CONSTRAINT fk_em_carbon_copy_recipients_email
        FOREIGN KEY (email_id) REFERENCES em_email (email_id)
);

CREATE TABLE em_blind_carbon_copy_recipients (
    email_id                     INTEGER NOT NULL,
    blind_carbon_copy_recipient  VARCHAR(255),
    CONSTRAINT fk_em_blind_carbon_copy_recipients_email
        FOREIGN KEY (email_id) REFERENCES em_email (email_id)
);

The status column stores the string representation of the EmailStatus enum. The body and failure_reason columns are declared as TEXT so that neither the message body nor a long error message is truncated; the exact type depends on the dialect (CLOB on H2 and Oracle, TEXT on PostgreSQL and MySQL). The data column in em_attachment stores raw binary content; its exact SQL type likewise depends on the dialect (BLOB on MySQL and H2, BYTEA on PostgreSQL). The email-to-attachment association is a unidirectional one-to-many with an explicit join column, so em_attachment carries the email_id foreign key and no join table is created.

Module Overview

Email Manager is structured as six Maven modules following the AppJars layered architecture:

Module Artifact ID Description
Model appjars-email-manager-model DTOs, EmailStatus enum, filter and sort classes, auto-configuration
Business API appjars-email-manager-business EmailService interface
Business Impl appjars-email-manager-business-impl Service implementation, MailSenderService, DefaultMailSenderImpl
Data API appjars-email-manager-data EmailDao and AttachmentDao interfaces
Data Impl appjars-email-manager-data-impl JPA entities and DAO implementations
Flow UI appjars-email-manager-flow Vaadin view and route configuration

A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.

Spring Auto-Configuration

Email Manager registers itself through Spring Boot's auto-configuration mechanism. It contributes two auto-configuration classes, each declared in its own module under:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

EmailManagerAutoConfiguration, in the model module, is the main entry point. It scans the com.appjars.emailmanager package for Spring components and JPA entities, excluding the classes annotated with @AutoConfiguration so that they are contributed once, through the imports file, rather than twice.

EmailManagerMailSenderAutoConfiguration, in the business implementation module, contributes the fallback mail sender. The business implementation module pulls in spring-boot-starter-mail, which provides the JavaMailSender infrastructure. This class is ordered after Spring Boot's own MailSenderAutoConfiguration and registers DefaultMailSenderImpl, built from the spring.mail.* properties described below, only when the application context contains no other JavaMailSender. An application that declares its own sender, or that lets Spring Boot build one from spring.mail.*, keeps it: the fallback backs off.

Configuration Properties

Mail Server

Property Default Description
spring.mail.host localhost SMTP server hostname
spring.mail.port 25 SMTP server port
spring.mail.protocol smtp Mail transport protocol
spring.mail.username (empty) SMTP authentication username
spring.mail.password (empty) SMTP authentication password
spring.mail.properties.mail.smtp.auth false Enable SMTP authentication
spring.mail.properties.mail.smtp.starttls.enable true Enable STARTTLS encryption

Email Manager

Property Default Description
com.appjars.emailmanager.url.views.emailcrudview em/list URL path of the email management view
com.appjars.emailmanager.from Comma-separated list of default sender addresses offered in the UI
com.appjars.emailmanager.attachments.max-size 26214400 Maximum size in bytes of a single attachment (25 MB)
com.appjars.emailmanager.attachments.max-files 5 Maximum number of attachments per email
com.appjars.emailmanager.attachments.accepted-types (see below) Comma-separated list of accepted attachment content types
spring.servlet.multipart.max-file-size 1MB Maximum size of individual attachment uploads. Raise it to at least the value of attachments.max-size
spring.servlet.multipart.max-request-size 10MB Maximum size of multipart requests. Should match max-file-size

The default value of com.appjars.emailmanager.attachments.accepted-types is:

application/pdf,
image/png,image/jpeg,image/gif,image/webp,image/svg+xml,
text/plain,text/csv,
application/zip,application/x-zip-compressed,
application/msword,
application/vnd.openxmlformats-officedocument.wordprocessingml.document,
application/vnd.ms-excel,
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,
application/vnd.ms-powerpoint,
application/vnd.openxmlformats-officedocument.presentationml.presentation

Setting the property to an empty value accepts any content type. All three bounds are enforced on the server after the file has been received, not only in the browser.

Email Status Lifecycle

An email moves through a defined sequence of statuses from creation to delivery:

stateDiagram-v2
    [*] --> CREATED        : email saved
    CREATED --> SEND_PENDING    : added to queue
    SEND_PENDING --> SEND_SUCCESSFUL : sent successfully
    SEND_PENDING --> SEND_FAILED     : sending failed
    CREATED --> SEND_SUCCESSFUL      : sent immediately
    CREATED --> SEND_FAILED          : immediate send failed
Hold "Alt" / "Option" to enable pan & zoom
Status Description
CREATED The email has been saved but not yet submitted for sending
SEND_PENDING The email is queued and will be picked up by the background sender
SEND_SUCCESSFUL The email was accepted by the SMTP server
SEND_FAILED The sending attempt was rejected or produced an error

Emails can be sent immediately via EmailService.attemptSend(), which transitions directly from CREATED to SEND_SUCCESSFUL or SEND_FAILED. Alternatively, an email can be queued by setting its status to SEND_PENDING, allowing the background MailSenderService to process it in a later batch.

When a delivery attempt fails, the type and message of the failure are stored on the email and can be inspected from the management view. A subsequent successful attempt clears them.

Background Sending

MailSenderService is a Spring-managed Runnable that processes emails in SEND_PENDING status. Each round it reads the first fifty queued emails and calls EmailService.attemptSend() for each one. It always re-reads the first page rather than advancing an offset, because a sent email leaves the SEND_PENDING filter and the result set shrinks underneath the cursor, which would make an advancing offset skip over the emails that moved up. The run ends when a round comes back empty, when a whole round makes no progress, or when the free-tier limit is reached. The total number of emails processed and successfully sent is logged at the end of the run.

Under a free licence the run aborts as soon as the daily send limit is reached, leaving the remaining emails queued for the next day.

The most straightforward way to run MailSenderService on a schedule is to register it as a Process Manager task. Because it implements Runnable and is a Spring component, it appears automatically in the Processes view if Process Manager is also integrated:

// No additional code required — MailSenderService is a @Component Runnable
// and is discovered by Process Manager's task scanning automatically.

Alternatively, it can be invoked directly via a standard Spring @Scheduled method. The schedule belongs to the host application; Email Manager reads no cron property of its own:

@Scheduled(cron = "${myapp.email.cron:0/30 * * * * *}")
public void sendPendingEmails() {
    mailSenderService.run();
}

Service API

EmailService is the primary interface for managing emails. It extends CrudService and ValidationSupport and exposes the following methods specific to the module:

// Attempt to send an email immediately via SMTP
// Returns true if sent successfully, false if sending failed
boolean attemptSend(EmailDto email);

// Place an email in the send queue
void enqueue(EmailDto email);

// Take an email out of the send queue
void dequeue(EmailDto email);

// Retrieve a paginated, filtered, and sorted stream of emails
Stream<EmailDto> getEmails(int offset, int limit, EmailFilter filter, List<EmailSort> sortOrder);

// Count emails matching a filter
int countEmails(EmailFilter filter);

// Return the most recently used recipient addresses, inspecting the given number of emails
Set<String> getRecentlyUsedAddresses(int emailsToInspect);

// Return the number of emails sent during the current day
int getTodayEmailCount();

enqueue() and dequeue() move an email in and out of the send queue, so a caller does not have to assemble the status transition itself. getEmails() takes its sort order as a list of EmailSort, each built from an EmailSortProperty — a closed set of sortable properties rather than a free-form property name.

attemptSend() is transactional: it updates the email status in the database regardless of whether the SMTP call succeeds or fails, ensuring the outcome is always persisted. On failure it also stores the type and message of the failure on the email; on success it clears them.

Every bean the module publishes is named through a constant in EmailManagerBeanNames. The service implementation is registered under EmailManagerEmailServiceImpl, and the DAO implementation under EmailManagerEmailDaoImpl. Inject them with the matching @Qualifier when the application context contains more than one candidate:

public MyService(
        @Qualifier(EmailManagerBeanNames.EMAIL_SERVICE) EmailService emailService) {
    this.emailService = emailService;
}

The implementation classes themselves are not public. Behaviour is replaced by publishing a competing EmailService, EmailDao or JavaMailSender bean, not by subclassing.

Validation is applied on save and update. Both creation and update validators check that the sender address is a valid email format and that at least one recipient address is present and valid. Invalid addresses cause a validation error before any persistence or sending occurs.

Free Licence Restrictions

Without a valid licence, Email Manager runs in free mode with a limit of five emails sent per day, exposed as the constant EmailManagerLimits.FREE_SENT_LIMIT. The counter covers successful deliveries only and resets at midnight in the server's default time zone.

Once the limit is reached:

  • EmailService.attemptSend() throws FreeLimitReachedException instead of contacting the SMTP server, and leaves the email in SEND_PENDING status so it can be sent once the counter resets.
  • MailSenderService stops its current run at the first FreeLimitReachedException.
  • In the management view, the Send now and Edit actions are disabled, and queued emails display a Limit reached badge in place of their status.

A restrictions bar above the grid shows the number of emails sent today against the limit whenever the module is running unlicensed. EmailService.getTodayEmailCount() exposes the same counter programmatically.

The daily allowance is claimed before the message is handed to the mail server and given back if the send does not happen, so concurrent callers cannot each read the same count and all send. The reservation is held per service instance: several application instances sharing one database are not serialised against each other.

Customisation

Replacing the Mail Sender

DefaultMailSenderImpl is a fallback: it is registered only when the application context contains no other JavaMailSender, and it reads its configuration from the spring.mail.* properties. To use a custom implementation — for example, one backed by a third-party mail API — declare it as a bean:

@Bean
public JavaMailSender customMailSender() {
    // return a custom JavaMailSender implementation
}

No @Primary is needed: the condition on the fallback means it steps aside as soon as another JavaMailSender is present, whether the application declares it or Spring Boot's own mail auto-configuration builds it from spring.mail.*.

Assigning a Router Layout

By default, Email Manager registers its view without a parent layout. To wrap it in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:

@Autowired
@Qualifier("EmailManagerRouteConfigurer")
private RouteConfigurer routeConfigurer;

@PostConstruct
public void configure() {
    routeConfigurer.setViewsRouterLayout(MainLayout.class);
}

The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.

Customising the View URL

The default URL path can be overridden in application.properties:

com.appjars.emailmanager.url.views.emailcrudview=myapp/emails

RouteConfigurer exposes both sides of that property. RouteConfigurer.URL_EMAIL_LIST_PROPERTY is the unresolved property expression, for use in an application's own @Value, and getEmailListsUrl() returns the route it resolved to:

@Value(RouteConfigurer.URL_EMAIL_LIST_PROPERTY)
private String emailListUrl;