Application to send out mass emails based on a list of recipients in a CSV file.
The application accepts UTF-8 CSV files (separator is ,) with a header line.
The columns firstname, surname, email, and salutation are required.
Columns are matched by name, so their order does not matter.
| firstname | surname | salutation | |
|---|---|---|---|
| Erika | Müller | erika@example.test | Liebe |
| Peter | Schmidt | peter@example.test | Lieber |
Enter the greeting prefix in salutation,
for example Liebe, Lieber, or Guten Tag, without the recipient's name.
The application uses this value as supplied; it does not infer a greeting from the
name or gender.
Every recipient must have a complete mail address in email. Empty values or
values consisting only of whitespace are not accepted. Rows with a missing or
blank email field are logged and skipped; they are not sent.
An example file is mailversendala-example.csv.
A Maven-generated site report is also available here.
Check out the application and build it with Java (17 or newer)
and the included Maven Wrapper. A separate Maven
installation is not required; the wrapper downloads the configured Maven version.
Use ./mvnw verify on macOS/Linux or .\mvnw.cmd verify on Windows to build and
run all unit and packaged-JAR integration tests.
Mail delivery uses Commons Email 2's Jakarta variant (2.0.0-M1, currently a milestone release)
with Eclipse Angus as the Jakarta Mail/Activation implementation. Local delivery tests use
GreenMail 2.1.x with the same mail stack; the executable JAR includes the runtime providers
but not GreenMail or the legacy javax.mail/javax.activation APIs.
$ git clone https://github.com/ottlinger/mailversendala.git
Cloning into 'mailversendala'...
...
$ ./mvnw verify
....
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.864 s
[INFO] Final Memory: 34M/481M
[INFO] ------------------------------------------------------------------------
$ java -jar target/mailversendala-2.0.0-SNAPSHOT-executable.jar
2024-12-22 00:48:00,714 DEBUG d.a.m.MailConfig [main] Reading Tamaya configuration ...
Dez. 22, 2024 12:48:00 AM org.apache.tamaya.spi.ServiceContextManager loadDefaultServiceProvider
INFORMATION: Using Service Context of type: org.apache.tamaya.spisupport.DefaultServiceContext
2024-12-22 00:48:00,755 DEBUG d.a.m.MailConfig [main] Configuration: DONE.
2024-12-22 00:48:00,755 INFO d.a.m.Mailversendala [main] **** MAILVERSENDALA: Starting .... ****
2024-12-22 00:48:00,757 INFO d.a.m.Mailversendala [main] Consuming CSV: mailversendala-example.csv
2024-12-22 00:48:00,758 INFO d.a.m.Mailversendala [main] mailversendala-example.csv
2024-12-22 00:48:00,758 WARN d.a.m.Mailversendala [main] Nothing to do - please configure your CSV path properly, either as environment variable or as a runtime parameter. Example: java -Dcsvpath=foo -jar fatJar.jar
2024-12-22 00:48:00,758 INFO d.a.m.Mailversendala [main] **** MAILVERSENDALA: Application shutdown .... ****
2024-12-22 00:48:00,758 INFO d.a.m.App [main] Mailversendala is shutting down. Bye Bye :-)
In case you want to use the application productively you need to configure it, please continue reading the following paragraph.
In order to properly configure the application you need to set some mail-related properties.
Use javaconfiguration.properties.template
as a starting point and pass the saved UTF-8 properties file using -Dconfig=path.
See the external configuration example below.
Technically the configuration is based on Apache Tamaya (incubating)
If you are running the application as a fat JAR you may want to overwrite each of the configuration keys either by environment variable or runtime property:
$ java -jar target/mailversendala-2.0.0-SNAPSHOT-executable.jar
// runs with default parameters from within the JAR
$ export csvpath=/tmp/my.csv; java -jar target/mailversendala-2.0.0-SNAPSHOT-executable.jar
// configuration set as environment variable
$ java -Dcsvpath=/tmp/my.csv -jar target/mailversendala-2.0.0-SNAPSHOT-executable.jar
// configuration option set as JVM parameter
You can keep your SMTP credentials in an external UTF-8 configuration file without
including them in the JAR. Pass the file explicitly using -Dconfig=path before
-jar. Simply placing the file beside the JAR is not sufficient.
Configuration precedence (highest first): JVM properties (-Dkey=value), environment
variables, the explicit config file, Tamaya configuration from the classpath, built-in
defaults. An unreadable or malformed explicit config file aborts startup; it does not
silently fall back to the JAR values. The loaded file's path is logged, but its contents
are not.
For example, organise your files as follows:
mailing/
├── mailversendala-2.0.0-SNAPSHOT-executable.jar
├── config/
│ └── META-INF/
│ └── javaconfiguration.properties
├── data/
│ └── recipients.csv
└── templates/
├── mailtemplate.html
├── mailtemplate.txt
└── attachments/
└── information.pdf # optional
config/META-INF/javaconfiguration.properties
host=smtp.example.com
port=465
username=user@example.com
password=YOUR_SMTP_PASSWORD
from=user@example.com
# Optional: send replies to a different address.
# replyto=replies@example.com
subject=Your invitation
# Test without actually sending any emails first.
demomode=true
csvpath=data/recipients.csv
templatepath=templates
htmltemplate=mailtemplate.html
plaintexttemplate=mailtemplate.txt
# Optional:
# attachments=attachments/information.pdfRelative paths are resolved from the working directory when the application starts,
not from the configuration file's directory. In this example, start from mailing/.
Alternatively, use absolute paths.
data/recipients.csv (UTF-8)
firstname,surname,email,salutation
Erika,Müller,erika@example.test,Dear
Peter,Schmidt,peter@example.test,Deartemplates/mailtemplate.html (UTF-8)
<html><body>
<p>${salutation} ${firstName},</p>
<p>Here is your invitation.</p>
</body></html>templates/mailtemplate.txt (UTF-8)
${salutation} ${firstName},
Here is your invitation.
From mailing/, start the application with (also works on Windows):
java -Dconfig=config/META-INF/javaconfiguration.properties \
-jar mailversendala-2.0.0-SNAPSHOT-executable.jarOn Windows, enter the command on a single line. The explicit file can have any name
and does not need a META-INF directory.
Legacy classpath configuration via -cp "config:mailversendala-2.0.0-SNAPSHOT-executable.jar"
and main class de.aikiit.mailversendala.App remains supported (use ; on Windows).
However, classpath order alone does not guarantee precedence between multiple Tamaya
configuration resources. Prefer -Dconfig for a deterministic external override.
Keep demomode=true for the initial test. Set demomode=false in the configuration
file only when you are ready to send emails. Replace the example recipient addresses
with real addresses before sending.
The password is still stored in plain text, but is not included in shell history or process arguments. Restrict access to the configuration on macOS or Linux:
chmod 700 config config/META-INF
chmod 600 config/META-INF/javaconfiguration.propertiesOn Windows, restrict access using the file and folder security settings. Do not commit the credentials file to version control or package it inside the JAR.
Every application start writes an additional UTF-8 log file into the current working directory, alongside the existing console output. A fixed run ID combines a timestamp and UUID, keeping the filename unique and stable even if logging is reconfigured. The filename is Windows-compatible, for example:
mailversendala-2026-11-08_16-30-42-123-789f7c38-35e9-44a4-8e32-f2a5e301a9ae.log
The INFO-level protocol contains per-recipient delivery/preparation messages, the final success/error report, and application errors. Demo-mode messages are explicitly marked as preparation rather than delivery. Writes are flushed immediately. Existing files are never overwritten; reinitializing the logger appends to the same run's protocol.
*.log is excluded by .gitignore, so these protocols are not checked into Git.
Files are retained until you archive or delete them yourself; there is no automatic
rotation or deletion. The working directory must be writable.
Protocols contain recipient email addresses and may contain personal data in error messages. Restrict access and keep them only as long as needed. These are application logs, not a guarantee that a message reached an inbox: a successful SMTP send means the SMTP server accepted it for further delivery.
The following parameters can be set:
| Configuration option | Description | Example/default setting |
|---|---|---|
| host | SMTP mail host | smtp.example.com |
| port | SMTP host port | 465 |
| username | SMTP plaintext username | user@example.com |
| password | SMTP plaintext password | chooseMeWisely |
| demomode | boolean, whether to not send mails | true |
| csvpath | fully qualified path to your CSV data input file | /tmp/mailversendala-example.csv |
| templatepath | Path to the external UTF-8 Velocity template directory | /tmp/mailversendala |
| htmltemplate | HTML template filename, relative to templatepath | mailtemplate.html |
| plaintexttemplate | Plain-text template filename, relative to templatepath | mailtemplate.txt |
| attachments | Optional comma-separated attachment paths, relative to templatepath | attachments/information.pdf |
| subject | full subject line of the example mail | Whatever you want is a subject line |
| from | mail address that is sent from | santa@cruz.com |
| replyto | Optional single Reply-To email address; empty or omitted means replies go to from |
replies@example.com (default: empty) |
| to | Optional recipient of one additional, unrendered test mail during the normal CSV run | xmas@man.com (default: empty) |
Set to to your own email address in the config file:
to=xmas@man.com
demomode=trueStart the application normally:
java -Dconfig=config/META-INF/javaconfiguration.properties \
-jar mailversendala-2.0.0-SNAPSHOT-executable.jarDuring the CSV run, one additional test mail is prepared for to and sent before
any personalized CSV messages. It contains the original HTML and plain-text template
contents, including unresolved placeholders such as ${firstName} or ${unknownField}.
Rendering errors for CSV recipients therefore do not prevent this test mail.
CSV recipients are still personalized separately; to does not replace them.
The report includes the additional test mail in its success/error counts.
The test mail uses the same SMTP settings, subject, Reply-To, images and attachments.
Template files and resource references must still be valid and readable; safety checks
are not bypassed (unresolved placeholders in image paths cannot identify a real file).
Omit to or leave it empty to disable the additional test mail. There is no default
test recipient.
Keep demomode=true for preparation without delivery. With demomode=false, both
the test mail and all successfully rendered CSV messages are sent; setting to
is not a test-only mode.
Each CSV recipient receives one email with both HTML and plain-text alternatives.
The mail client chooses which version to display. Both bodies are rendered with Apache
Velocity; the configured subject is used unchanged and both alternatives use UTF-8.
Create two UTF-8 files outside the JAR, for example:
/tmp/mailversendala/welcome.html
<html><body>
<p>${salutation} ${firstName},</p>
<p>Your invitation is ready.</p>
</body></html>/tmp/mailversendala/welcome.txt
${salutation} ${firstName},
Your invitation is ready.
Configure the directory and filenames in javaconfiguration.properties, or override
these settings using JVM properties before -jar:
java -Dtemplatepath=/tmp/mailversendala \
-Dhtmltemplate=welcome.html \
-Dplaintexttemplate=welcome.txt \
-Dcsvpath=/tmp/recipients.csv \
-jar target/mailversendala-2.0.0-SNAPSHOT-executable.jarSMTP credentials and from must also be configured. Demo mode is enabled by default;
set demomode=false only when ready to deliver the messages.
Available recipient variables are ${salutation}, ${firstName}, ${lastName},
and ${email}. ${firstname} and ${surname} are aliases matching the CSV column
names. With the CSV above, ${salutation} ${firstName} renders Liebe Erika for
Erika and Lieber Peter for Peter, in both HTML and plain text. The template supplies
the separating space and any punctuation; the CSV value supplies the greeting.
References in HTML are HTML-escaped automatically; plain-text values are not HTML-escaped. Use these variables in HTML text or double-quoted attributes, not as JavaScript or unvalidated URLs.
Both templates are loaded and compiled once per run. Missing or syntactically invalid
templates abort the run before delivery; rendering or SMTP errors are counted per
recipient. Unknown variables are rendering errors rather than being sent as literal
placeholders. Templates can use Velocity directives such as #if and #foreach;
only use trusted template files. The template-based sending flow never downloads remote
images or attachments.
The normal CSV run checks every successfully rendered HTML body with Jsoup before
any SMTP delivery, including delivery of the additional test mail. Errors such as
mismatched closing tags, invalid nesting, duplicate attributes or unterminated quotes
abort the entire run. Ordinary unknown HTML tags such as <spna> instead of <span>
are also rejected, since Jsoup would otherwise accept them silently.
Diagnostics include the HTML template path, the affected recipient, and line/column positions in the rendered HTML. Velocity directives may change line numbers relative to the original template. Up to 20 errors are reported per failed body. For example:
Invalid HTML markup in templates/welcome.html (recipient: ada@example.test) ...
line/column 2:1: Unknown HTML tag <spna>
Full HTML documents and body fragments are supported, as are HTML void elements such
as <br> and <img> without XML-style closing tags. The check does not rewrite the
HTML or fetch external resources. It also runs in demo mode, so demomode=true lets
you diagnose template problems without sending any mail.
This is a typo/parser check, not a complete HTML conformance validator or a guarantee of correct rendering in every mail client. Custom elements with hyphenated names, namespaced markup, and Outlook conditional comments are allowed. Not every misspelled attribute can be detected.
Only rendered CSV bodies are checked: the additional unrendered test mail retains placeholders and Velocity directives intentionally. Checking those directives as finished HTML could reject a valid template. If a CSV body's markup check fails, no messages, including that test mail, are sent. Existing image and attachment checks still apply to the unrendered test mail.
Images referenced by HTML <img src="..."> elements are automatically embedded in the
email using MIME Content-IDs. The image is sent with the message rather than downloaded
from a website. PDF documents, images, and other files can also be added as ordinary,
downloadable attachments. HTML and plain-text alternatives are preserved in both cases.
For example:
/path/to/templates/
├── welcome.html
├── welcome.txt
├── images/
│ └── logo.png
└── attachments/
└── information.pdf
In welcome.html:
<p>Hello ${firstName} ${lastName},</p>
<img src="images/logo.png" alt="Our logo">
<p>Please see the attached information document.</p>Configure the ordinary attachment separately:
templatepath=/path/to/templates
htmltemplate=welcome.html
plaintexttemplate=welcome.txt
attachments=attachments/information.pdfThe equivalent JVM override is -Dattachments=attachments/information.pdf before
-jar. To attach several files, separate their paths with commas, for example
attachments=attachments/information.pdf,images/logo.png. Leave the setting empty
for no ordinary attachments. Repeated paths to the same file are attached only once.
Merely linking a PDF with <a href="..."> does not attach it. Attachment configuration
is shared by all recipients and is not interpreted as a Velocity template.
All image and attachment paths are relative to templatepath, even when the HTML
template itself is in a subdirectory. UTF-8 filenames and percent-encoded paths are
supported; use / as the separator. Filenames containing commas cannot be listed in
attachments. MIME types are inferred from filenames, so use appropriate extensions.
Inline resources must have an image MIME type.
Absolute paths, paths escaping the template directory (including symlinks), remote
URLs, file:/data:/pre-existing cid: image sources, and query strings/fragments
are rejected. srcset is not supported; use img src. CSS background images and other
HTML resource references are not automatically embedded. Ordinary web hyperlinks
remain unchanged and are not fetched by the application.
Before the first SMTP delivery, the application renders the recipients and validates all referenced images and configured attachments. Missing or forbidden resources abort the entire run, including when referenced only by a later recipient. Resource bytes are snapshotted in memory once per unique file, so a file changed or deleted afterward does not change already validated content. Allow enough memory for the files and the rendered messages in a large mailing. Demo mode performs these checks without sending.
For a complete working example, copy the existing template directory, including its
img/ and attachments/ subdirectories:
mkdir -p /tmp/mailversendala
cp -R src/test/resources/template/. /tmp/mailversendala/Use htmltemplate=mailtemplate.html, plaintexttemplate=mailtemplate.txt, and
attachments=attachments/information.pdf. The example HTML embeds a local logo;
the example PDF is a harmless information document. Image attribution is preserved
in src/test/resources/template/img/readme.txt.
In order to properly integrate the project into your IDE you need to setup lombok support - please follow the guidelines about integration, e.g. Eclipse integration.
See CHANGELOG.md.
© P. Ottlinger, 2017