Apache James 3.9.1 on AWS User Guide
Overview
This guide covers launching and running the Apache James 3.9.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Mail Servers and Newsletters listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Apache James (Java Apache Mail Enterprise Server) is the open source mail server of the Apache Software Foundation: an SMTP transfer agent for sending and receiving, IMAP mailbox access, Sieve filtering, and a programmable mailet container for routing, rewriting and filtering mail. This image ships the Apache 2.0 licensed 3.9.1 release in its single node distribution, so the mailbox store (JPA over embedded Apache Derby), the full text search index (embedded Apache Lucene) and the mail queue (embedded Apache ActiveMQ) all run inside one JVM. There is no database server, search cluster or message broker to operate.
The cloudimg image adds:
- No credential ships in the image. First boot generates this instance's TLS keystore, its postmaster mailbox password, its datastore credentials and its admin API password, and writes them to
/root/apache-james-credentials.txt(root only). None of the upstream sample passwords opens anything, and James does not start until first boot has finished. - Authentication only over TLS. Submission on 587 (STARTTLS) and 465 (implicit TLS) requires SMTP AUTH, and the image adds a small hook so an AUTH sent before STARTTLS is refused even with the right password (upstream only hides the AUTH offer). IMAP on 143 refuses LOGIN and AUTHENTICATE until STARTTLS; 993 is implicit TLS. Inbound mail on 25 is offered STARTTLS.
- Never an open relay. The upstream setting that lets anything on the machine relay without authentication is removed. The build proves it with a trap listener: an unauthenticated relay attempt is refused and nothing arrives, while an authenticated one does.
- A locked admin surface. The webadmin REST API listens on
127.0.0.1:8000only and requires this instance's password. JMX is switched off, because James cannot keep its JMX RMI port on loopback. Thesudo james-adminhelper covers everyday tasks and never takes a password on its command line. - Walled off from instance metadata. The James JVM cannot reach the EC2 instance metadata service, and the AMI requires IMDSv2.
- Verified and current. James 3.9.1 is checked against Apache's SHA-512 and release signature at build time. It replaces 3.9.0 for its ActiveMQ, Netty, BouncyCastle, Jackson, jsoup and Logback security fixes (see Release notes and known advisories). Ubuntu 24.04 LTS is fully patched at build time with unattended security upgrades on, and OpenJDK 21 comes from the Ubuntu archive.
Apache James and Apache are trademarks of The Apache Software Foundation. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by The Apache Software Foundation.
Before you start: EC2 limits outbound port 25
Amazon EC2 restricts outbound traffic on TCP port 25 from instances by default. The build of this image measured it: port 25 to a public mail exchanger was blocked while port 587 was reachable. You can ask AWS to lift the restriction for your account with the request to remove email sending limitations form.
| Works with the default EC2 restriction | |
|---|---|
| Receiving mail on port 25 from other mail servers | Yes, inbound is unaffected |
| IMAP mailbox access on 143 and 993 | Yes |
| Authenticated submission from your mail clients on 587 and 465 | Yes |
| Sending directly to recipient mail servers on port 25 | Only after AWS lifts the limit |
| Sending through an authenticated smart host on port 587 | Yes (see Sending mail out through a relay) |
Launching the instance
- Subscribe to the listing on AWS Marketplace and choose Apache James 3.9.1 on Ubuntu 24.04 as the software version.
- Use m5.large (2 vCPU, 8 GiB) or larger. The JVM heap is capped at 1 GiB, which leaves room for the operating system and page cache with no swap.
- Choose your key pair. The AMI requires IMDSv2.
- Use a security group that opens only what you intend to serve:
| Port | Protocol | Purpose | Open to |
|---|---|---|---|
| 22 | TCP | SSH administration | Your management address only |
| 25 | TCP | Inbound SMTP from other mail servers | The internet, if you receive external mail |
| 587 | TCP | Authenticated submission, STARTTLS | Your users' networks |
| 465 | TCP | Authenticated submission, implicit TLS | Your users' networks |
| 143 | TCP | IMAP, STARTTLS | Your users' networks |
| 993 | TCP | IMAP, implicit TLS | Your users' networks |
Never open port 8000. The webadmin API is bound to loopback and reached over an SSH tunnel. POP3, LMTP, ManageSieve and JMX are switched off.
First boot runs apache-james-firstboot.service: it generates this instance's secrets and TLS keystore, creates the default mail domain and the postmaster mailbox, and starts James. It usually finishes in under a minute.
Connecting to your instance
| Operating system | SSH user |
|---|---|
| Ubuntu 24.04 | ubuntu |
ssh -i /path/to/your-key.pem ubuntu@<public-ip>
Confirming the server is running
systemctl is-active apache-james.service
systemctl show -p Result --value apache-james-firstboot.service
apache-james.service reports active and the first boot unit reports success.
Check the exact release and the listening ports:
unzip -p /opt/james/james-server-jpa-app.jar META-INF/MANIFEST.MF | tr -d '\r' | grep -E '^(Implementation-Version|Main-Class):'
sudo ss -lntp | awk 'NR>1 && /java/ {print $4}' | sort -u
You see Implementation-Version: 3.9.1 and Main-Class: org.apache.james.JPAJamesServerMain. Ports 25, 143, 465, 587 and 993 listen on all addresses. Only two java listeners are on loopback: 127.0.0.1:8000 (webadmin) and one ephemeral port, the embedded ActiveMQ broker's internal transport.
Ask James whether it is healthy (the helper sends the admin password for you):
sudo james-admin health
The answer is a JSON document with "status": "healthy".
Your per instance credentials
sudo stat -c '%A %U:%G %n' /root/apache-james-credentials.txt
sudo grep -E '^JAMES_(MAIL_DOMAIN|POSTMASTER_ADDRESS|WEBADMIN_URL|SUBMISSION_ENDPOINT|IMAPS_ENDPOINT)=' /root/apache-james-credentials.txt
The file is -rw------- root:root and records:
| Key | What it is |
|---|---|
JAMES_MAIL_DOMAIN |
The default mail domain created for this instance (its EC2 host name) |
JAMES_POSTMASTER_ADDRESS |
The only mailbox that exists, postmaster@ that domain |
JAMES_POSTMASTER_PASSWORD |
That mailbox's password, generated on this instance |
JAMES_WEBADMIN_PASSWORD |
The webadmin API password, sent as the Password header |
JAMES_KEYSTORE_SECRET |
The password protecting this instance's TLS keystore |
JAMES_DB_USER / JAMES_DB_PASSWORD |
The embedded datastore credentials |
JAMES_SUBMISSION_ENDPOINT / JAMES_IMAPS_ENDPOINT |
Where to point a mail client |
To read the postmaster password when you need it:
sudo grep -E '^JAMES_POSTMASTER_PASSWORD=' /root/apache-james-credentials.txt | cut -d= -f2-
Two instances launched from this AMI never share any of these values.
Proving mail actually flows
The image ships a self test that does a real round trip and checks that the things which must not work really do not:
sudo /usr/local/sbin/james-selftest.sh
It checks the authenticated health endpoint and that webadmin refuses a missing or wrong password with 401. It then sends AUTH with the right password on 587 before STARTTLS (refused with 530) and after it (235), submits a message carrying a random nonce, and reads it back over IMAPS 993. It confirms IMAP 143 refuses LOGIN before STARTTLS and accepts it after, refuses an unauthenticated relay to an external domain, refuses wrong passwords on 587 and 993, and still accepts inbound mail on 25 to a local mailbox. It removes its probe message and ends with:
selftest: open-relay attempt refused (550 5.7.1 Requested action not taken: relaying denied)
...
SELFTEST_OK: webadmin auth, AUTH and IMAP login only after TLS, real delivery and retrieval, relay refusal, credential refusals and inbound reception all verified
Authentication only after TLS
Before STARTTLS, port 587 does not offer AUTH:
{ sleep 1; printf 'EHLO check.example\r\n'; sleep 1; printf 'QUIT\r\n'; } | nc -w 5 127.0.0.1 587 | tr -d '\r'
220 Apache James SMTP Server ready
250-ip-172-31-5-9 Hello check.example [127.0.0.1])
250-PIPELINING
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-SIZE 52428800
250 STARTTLS
221 2.0.0 ip-172-31-5-9 Service closing transmission channel
There is STARTTLS and no AUTH line. After STARTTLS the same port offers AUTH LOGIN PLAIN:
printf 'EHLO check.example\r\nQUIT\r\n' | timeout 10 openssl s_client -quiet -starttls smtp -connect 127.0.0.1:587 2>/dev/null | tr -d '\r' | grep -E '^250'
Upstream James only hides the AUTH offer before TLS; a client that sent AUTH anyway was accepted. This image adds RequireTlsForAuthHook to the two submission listeners, so such an attempt gets 530 5.7.0 Must issue a STARTTLS command first. Its source is in the image:
grep -c RequireTlsForAuthHook /var/lib/james/conf/smtpserver.xml
ls /usr/share/doc/cloudimg/RequireTlsForAuthHook.java /opt/james/extensions-jars/
This server is not an open relay
Apache James's sample configuration sets <authorizedAddresses>127.0.0.0/8</authorizedAddresses>, which lets any process on the machine relay to any destination without authentication. This image removes it, so SMTP AUTH is required before anything is relayed anywhere, loopback included. Receiving is unaffected: mail from other servers to your own mailboxes is accepted.
sudo grep -c authorizedAddresses /var/lib/james/conf/smtpserver.xml || echo "0: relaying requires authentication"
An authenticated user also cannot send with someone else's address as the sender: James answers 503 5.7.1 Incorrect Authentication for Specified Email Address.
Administering the server
James has no web interface. Administration is the webadmin REST API on 127.0.0.1:8000, which requires this instance's password in a Password header. Without it the API answers 401:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/domains
The bundled james-admin helper reads the password from the credentials file and sends it for you. Run it with sudo:
sudo james-admin
sudo james-admin domains
sudo james-admin users
Adding your own domain and mailboxes
Add the domain you receive mail for, then create mailboxes in it. add-user generates a strong password and prints it once. Passwords are never taken on the command line (sudo logs command lines):
sudo james-admin add-domain example.net
sudo james-admin add-user alice@example.net
sudo james-admin users
sudo james-admin remove-user alice@example.net
sudo james-admin remove-domain example.net
domain example.net added
mailbox alice@example.net created
password (shown once, store it now): <generated-password>
To set a password of your own, pipe it on standard input (or run the command in a terminal and type it at the prompt):
sudo james-admin set-password alice@example.net
Address length limit. This distribution stores domains and mailbox addresses in columns of at most 100 characters.
james-adminchecks the length and says so plainly.
Reaching the API from your workstation
ssh -i /path/to/your-key.pem -L 8000:127.0.0.1:8000 ubuntu@<public-ip>
Then send the password as a header from a file rather than on the command line, for example curl -H @webadmin.hdr http://127.0.0.1:8000/domains, where webadmin.hdr contains Password: <the JAMES_WEBADMIN_PASSWORD value>. The webadmin API reference lists every route.
Why JMX is off
James's JMX registry can be bound to loopback, but it exports its RMI server object on a second, random port on every interface, and the JDK's own agent does the same. JMX also carries James's JMX advisories (CVE-2023-26269, CVE-2023-51518), so this image disables it (jmx.enabled=false in /var/lib/james/conf/jmx.properties). As a result james-cli does not connect; use james-admin or the webadmin API instead.
grep -E '^jmx\.enabled' /var/lib/james/conf/jmx.properties
Connecting a mail client
| Setting | Value |
|---|---|
| Incoming (IMAP) | Your instance's address, port 993, SSL/TLS |
| Incoming (IMAP, alternative) | Your instance's address, port 143, STARTTLS |
| Outgoing (SMTP submission) | Your instance's address, port 587, STARTTLS |
| Outgoing (alternative) | Your instance's address, port 465, SSL/TLS |
| Username | The full mailbox address, for example alice@example.net |
| Password | The mailbox password |
The image generates a self signed certificate for the instance's own name, so clients warn on first connection until you install a real one.
Installing a real TLS certificate
Build a PKCS12 keystore with the alias james from the certificate and key issued for the name your users connect to. Keep the new keystore password in a root-only file rather than on the command line:
sudo install -m 0600 /dev/null /root/keystore.pass
sudo sh -c 'openssl rand -hex 24 > /root/keystore.pass'
sudo openssl pkcs12 -export -in /path/to/fullchain.pem -inkey /path/to/privkey.pem \
-name james -out /var/lib/james/conf/keystore.new -passout file:/root/keystore.pass
Put that password into the <secret> elements of /var/lib/james/conf/smtpserver.xml and imapserver.xml with an editor, then swap the keystore in and restart:
sudo mv /var/lib/james/conf/keystore.new /var/lib/james/conf/keystore
sudo chown james:james /var/lib/james/conf/keystore && sudo chmod 0640 /var/lib/james/conf/keystore
sudo systemctl restart apache-james.service
Sending mail out through a relay
Because EC2 limits outbound port 25, send through an authenticated smart host on 587 (Amazon SES or your mail provider). Add the gateway settings to the RemoteDelivery mailet in /var/lib/james/conf/mailetcontainer.xml:
<mailet match="All" class="RemoteDelivery">
<outgoingQueue>outgoing</outgoingQueue>
<delayTime>5000, 100000, 500000</delayTime>
<maxRetries>3</maxRetries>
<maxDnsProblemRetries>0</maxDnsProblemRetries>
<deliveryThreads>10</deliveryThreads>
<sendpartial>true</sendpartial>
<bounceProcessor>bounces</bounceProcessor>
<gateway>smtp.your-relay-provider.net</gateway>
<gatewayPort>587</gatewayPort>
<gatewayUsername>your-relay-username</gatewayUsername>
<gatewayPassword>your-relay-password</gatewayPassword>
<startTLS>true</startTLS>
</mailet>
Then restart and check:
sudo systemctl restart apache-james.service
sudo james-admin health
Publish the SPF, DKIM and DMARC records your relay provider gives you; without them most large mailbox providers reject or quarantine your mail.
DNS records for receiving mail
For each domain you add, publish an MX record pointing at a host name whose A record is the instance's public address. Use an Elastic IP so the address survives a stop and start, and set its reverse DNS (PTR) record to that host name in the EC2 console (Elastic IPs, Update reverse DNS). Many receiving servers check it.
Where everything lives
ls /opt/james
sudo ls /var/lib/james /var/lib/james/var
| Path | Contents |
|---|---|
/opt/james |
The Apache James distribution and conf.template, the inert placeholder configuration. Owned by root, read only to the service. |
/opt/james/extensions-jars |
The cloudimg AUTH-after-TLS hook |
/var/lib/james/conf |
The live configuration |
/var/lib/james/var/store |
The embedded Derby mailbox database and the ActiveMQ queue store |
/var/lib/james/var/mail |
Mail repositories (errors, relay denied, address errors) |
/var/lib/james/var/log |
james.log and its archives |
/var/lib/james |
The Lucene search index |
/root/apache-james-credentials.txt |
This instance's generated credentials, mode 0600 |
Logs and troubleshooting
sudo tail -20 /var/lib/james/var/log/james.log
sudo journalctl -u apache-james.service -n 30 --no-pager
If apache-james.service shows a failed start condition, first boot has not completed:
sudo systemctl status apache-james-firstboot.service --no-pager | head -12
sudo tail -20 /var/log/cloudimg-firstboot.log
Backups
Everything stateful is under /var/lib/james. For a consistent copy, stop James, archive the directory and start it again:
sudo systemctl stop apache-james.service
sudo tar -czf /root/james-backup-$(date +%F).tar.gz -C /var/lib james
sudo systemctl start apache-james.service
EBS snapshots of the root volume (or AWS Backup) are the simplest whole-instance option.
Updating
Ubuntu security updates, including the OpenJDK runtime, are applied by unattended-upgrades:
systemctl is-enabled unattended-upgrades.service
grep 'Unattended-Upgrade' /etc/apt/apt.conf.d/20auto-upgrades
For Apache James itself, cloudimg publishes a new version of the listing for each upstream release. Launch the new version alongside, move mail across with your usual IMAP tooling, then retire the old instance.
Release notes and known advisories
3.9.1 replaces 3.9.0. The 3.9.1 release upgrades the libraries James ships and fixes these advisories: ActiveMQ 6.1.6 to 6.2.7 (CVE-2026-34197, CVE-2026-42588, CVE-2026-45505, CVE-2026-41044, CVE-2026-40466, CVE-2026-39304, CVE-2026-33227, CVE-2026-49270, CVE-2026-41043), BouncyCastle 1.81 to 1.85 (CVE-2026-8763, CVE-2025-14813, CVE-2026-13506, CVE-2026-0636, CVE-2026-5588), Netty 4.1.126 to 4.1.135 (CVE-2025-67735, CVE-2026-44249, CVE-2026-50010, CVE-2026-45416, CVE-2026-42583 and others), jsoup 1.20.1 to 1.23.2 (CVE-2026-75140, CVE-2026-71497), Logback 1.5.18 to 1.5.38 (CVE-2025-11226, CVE-2026-10532, CVE-2026-9828, CVE-2026-1225), Jackson 2.19.1 to 2.22 (CVE-2026-18401, CVE-2026-68494, CVE-2026-59888, CVE-2026-59889), commons-configuration2 2.15.0 (CVE-2026-45205) and Jetty 12.1.10. It also turns webadmin password authentication on by default.
Advisories still present in the 3.9.1 libraries. A vulnerability scan of this AMI may list the following. None is reachable in this configuration:
| Advisory | Library | Why it is not reachable here |
|---|---|---|
| CVE-2026-75595, CVE-2026-75596 | Netty 4.1.135 handler (SNI) | James does not use Netty's SNI handler |
| CVE-2026-59901 | Netty codec (Bzip2 decoder) | Not used by the mail protocols |
| CVE-2026-73508 | Netty DNS codec | James resolves DNS with dnsjava |
| CVE-2026-55851, CVE-2026-59919 | Netty HAProxy decoder | Used only when proxyRequired is enabled; it is off |
| CVE-2026-59898, CVE-2026-56746, CVE-2026-55831, CVE-2026-59903, CVE-2026-59921, CVE-2026-56745, CVE-2026-55833, CVE-2026-59899, CVE-2026-56819, CVE-2026-59900 | Netty HTTP, HTTP/2, SPDY, CORS and WebSocket codecs | James serves no HTTP over Netty; webadmin runs on Jetty on 127.0.0.1 |
| CVE-2026-89425, CVE-2026-89407, CVE-2026-91777, CVE-2026-91776 | Jackson 2.22 | JSON is parsed only by the loopback, password protected webadmin API |
These clear when Apache James ships the fixed libraries; cloudimg then publishes a new version.
Licensing
Apache James is free and open source software under the Apache License 2.0, shipped unmodified apart from configuration. The cloudimg AUTH-after-TLS hook is a separate, small extension whose source is in /usr/share/doc/cloudimg/. The bundled libraries (Apache Derby, Lucene and ActiveMQ, Netty, Jetty, Guice, Logback, BouncyCastle) are permissively licensed, and the Java runtime is Ubuntu's openjdk-21-jre-headless package.
head -3 /opt/james/LICENSE
Support
cloudimg supports this image through the AWS Marketplace listing and at cloudimg.co.uk. The other products in this listing are on the Mail Servers and Newsletters hub.