You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by "prosgarz35 (via GitHub)" <gi...@apache.org> on 2026/10/06 04:16:48 UTC

[PR] Support PEM and Keystore TLS Certificates for JMAP Server [james-project]

prosgarz35 opened a new pull request, #3240:
URL: https://github.com/apache/james-project/pull/3240

   ## Summary
   
   This pull request introduces support for configuring TLS on the JMAP server directly using `.pem` certificates and private keys, as well as Java Keystores (JKS / PKCS12), matching the existing PEM TLS support found in IMAP, POP3, and SMTP protocols.
   
   Prior to this change, JMAP relied on plain HTTP, requiring an external reverse proxy (such as Nginx or Traefik) to terminate TLS. With this enhancement, administrators can terminate TLS natively within Apache James JMAP using standard PEM certificates (e.g. Let's Encrypt certificates) or existing keystores.
   
   ## Key Changes
   
   1. **`server/protocols/jmap` (`pom.xml`):**
      - Added `ayza-for-pem` (`io.github.hakky54:ayza-for-pem`) dependency to parse PEM-encoded certificates and private keys (PKCS#1, PKCS#8, EC, RSA) via `PemUtils`, consistent with the rest of James.
      - Added `james-server-filesystem-api` dependency to resolve certificate/keystore resource locations (`file://`, `classpath://`, etc.).
   
   2. **`JMAPConfiguration.java`:**
      - Added TLS configuration options:
        - `tls.keystoreURL` / `keystore`
        - `tls.keystoreType` / `keystoreType` (optional, default: JKS)
        - `tls.privateKey` / `privateKey`
        - `tls.certificates` / `certificates`
        - `tls.secret` / `secret` (optional password for keystore or encrypted private key)
      - Added `isTlsEnabled()` helper method checking if keystore or PEM certificate/key pair is supplied.
      - Preserved backward compatibility for testing constructors (`@VisibleForTesting`).
   
   3. **`JMAPServer.java`:**
      - Injected `FileSystem` into the Guice constructor.
      - Configured Reactor Netty `HttpServer` with `.secure(...)` when TLS is enabled.
      - Built Netty `SslContext`:
        - **PEM Mode:** Parsed certificate chain via `PemUtils.loadCertificate(...)` and private key via `PemUtils.loadPrivateKey(...)`, configured via `SslContextBuilder.forServer(privateKey, certificates)`.
        - **Keystore Mode:** Loaded keystore via `KeyStoreUtils.loadKeyStore(...)`, configured via `SslContextBuilder.forServer(KeyManagerUtils.createKeyManager(keyStore, password))`.
        - **RFC Compliance:** Explicitly enforced TLS 1.2 and TLS 1.3 protocols (`RFC 5246` and `RFC 8446`), avoiding insecure older versions (SSLv3, TLS 1.0, TLS 1.1).
   
   4. **`JMAPModule.java`:**
      - Added parsing of TLS parameters from `jmap.properties`.
   
   5. **Tests:**
      - Added unit tests in `JMAPConfigurationTest` for verifying TLS properties and builder options.
      - Added integration test cases in `JMAPServerTest` verifying successful HTTPS startup and request handling with both PEM certificates/keys and Keystore configurations.
   
   ## Configuration Example (`jmap.properties`)
   
   ### Option A: PEM Certificates (e.g., Let's Encrypt)
   ```properties
   # Enable JMAP
   enabled=true
   port=8443
   
   # TLS with PEM files
   tls.certificates=file://conf/cert.pem
   tls.privateKey=file://conf/privkey.pem
   # tls.secret=optional_key_password
   ```
   
   ### Option B: Keystore
   ```properties
   # Enable JMAP
   enabled=true
   port=8443
   
   # TLS with Keystore
   tls.keystoreURL=file://conf/keystore
   tls.keystoreType=PKCS12
   tls.secret=mysecretpassword
   ```
   
   ## Adherence to Principles
   - **KISS:** Directly leverages Netty's `SslContextBuilder.forServer(PrivateKey, X509Certificate[])` without superfluous wrapper layers.
   - **DRY:** Reuses `ayza-for-pem` and `FileSystem` mechanisms already adopted across James protocols.
   - **YAGNI:** Only includes server-side TLS termination required for HTTPS/JMAP, omitting unnecessary client-certificate authentication or dynamic hot-swapping complexes.
   - **RFC:** Strictly restricts protocol negotiation to TLSv1.2 and TLSv1.3.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


Re: [PR] Support PEM and Keystore TLS Certificates for JMAP Server [james-project]

Posted by "chibenwa (via GitHub)" <gi...@apache.org>.
chibenwa commented on code in PR #3240:
URL: https://github.com/apache/james-project/pull/3240#discussion_r4192231781


##########
server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java:
##########
@@ -114,11 +119,62 @@ public Builder maximumSendSize(Optional<Long> maximumSendSize) {
             return this;
         }
 
+        public Builder keystore(String keystore) {
+            this.keystore = Optional.ofNullable(keystore).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder keystore(Optional<String> keystore) {
+            this.keystore = keystore.filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder keystoreType(String keystoreType) {
+            this.keystoreType = Optional.ofNullable(keystoreType).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder keystoreType(Optional<String> keystoreType) {
+            this.keystoreType = keystoreType.filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder privateKey(String privateKey) {
+            this.privateKey = Optional.ofNullable(privateKey).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder privateKey(Optional<String> privateKey) {
+            this.privateKey = privateKey.filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder certificates(String certificates) {
+            this.certificates = Optional.ofNullable(certificates).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder certificates(Optional<String> certificates) {
+            this.certificates = certificates.filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder secret(String secret) {
+            this.secret = Optional.ofNullable(secret).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder secret(Optional<String> secret) {
+            this.secret = secret.filter(s -> !s.isEmpty());
+            return this;
+        }
+
         public JMAPConfiguration build() {
             Preconditions.checkState(enabled.isPresent(), "You should specify if JMAP server should be started");

Review Comment:
   Mixing keystore and pem constructs should fail
   Enabling TLS without a complete keystore or pem should fail
   Here
   With explicit messages



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


Re: [PR] Support PEM and Keystore TLS Certificates for JMAP Server [james-project]

Posted by "chibenwa (via GitHub)" <gi...@apache.org>.
chibenwa commented on code in PR #3240:
URL: https://github.com/apache/james-project/pull/3240#discussion_r4192231781


##########
server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPConfiguration.java:
##########
@@ -114,11 +119,62 @@ public Builder maximumSendSize(Optional<Long> maximumSendSize) {
             return this;
         }
 
+        public Builder keystore(String keystore) {
+            this.keystore = Optional.ofNullable(keystore).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder keystore(Optional<String> keystore) {
+            this.keystore = keystore.filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder keystoreType(String keystoreType) {
+            this.keystoreType = Optional.ofNullable(keystoreType).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder keystoreType(Optional<String> keystoreType) {
+            this.keystoreType = keystoreType.filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder privateKey(String privateKey) {
+            this.privateKey = Optional.ofNullable(privateKey).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder privateKey(Optional<String> privateKey) {
+            this.privateKey = privateKey.filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder certificates(String certificates) {
+            this.certificates = Optional.ofNullable(certificates).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder certificates(Optional<String> certificates) {
+            this.certificates = certificates.filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder secret(String secret) {
+            this.secret = Optional.ofNullable(secret).filter(s -> !s.isEmpty());
+            return this;
+        }
+
+        public Builder secret(Optional<String> secret) {
+            this.secret = secret.filter(s -> !s.isEmpty());
+            return this;
+        }
+
         public JMAPConfiguration build() {
             Preconditions.checkState(enabled.isPresent(), "You should specify if JMAP server should be started");

Review Comment:
   Mixing keystore and pem constructs should fail
   Enabling TLS without a incomplete keystore or pem should fail
   Here
   With explicit messages



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


Re: [PR] Support PEM and Keystore TLS Certificates for JMAP Server [james-project]

Posted by "chibenwa (via GitHub)" <gi...@apache.org>.
chibenwa commented on PR #3240:
URL: https://github.com/apache/james-project/pull/3240#issuecomment-6128071057

   Hello @prosgarz35 
   
   Would you have time to handle the feedback ?
   
   Best regards,
   
   Benoit


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org