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"
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
| 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()throwsFreeLimitReachedExceptioninstead of contacting the SMTP server, and leaves the email inSEND_PENDINGstatus so it can be sent once the counter resets.MailSenderServicestops its current run at the firstFreeLimitReachedException.- 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;