Claude 7c2fb0142e Add OpenPGP and S/MIME signing of outgoing mail
mail.pgp.signing.type selects NONE (default), OPENPGP or SMIME, with one
key/certificate per From address.

- OPENPGP: plaintext copies become PGP/MIME multipart/signed (RFC 3156 s5,
  detached signature over the exact content entity bytes); encrypted copies
  are signed inside the encryption (combined method)
- SMIME: plaintext copies become S/MIME multipart/signed (RFC 8551, via
  Bouncy Castle bcjmail); encrypted copies are S/MIME-signed, then
  PGP/MIME-encrypted
- signed bodies are attached as raw bytes so they reach the wire unchanged;
  shared MIME helpers move to MimeEntities
- keys are validated at startup (signing capability, address match,
  validity, passphrase / key-certificate match)
- mail.pgp.signing.required refuses mail from addresses without a key
  (MailSigningException); RELAY mode signs before handing over
- SendReport.signature(), metric tag signature, refused{cause}
- core test-jar shares key/certificate and verification helpers
- end-to-end tests verify signatures on the bytes received over SMTP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G71TQ5jDETVwdUJxGAwaiT
2026-10-15 09:49:45 +00:00
2026-10-12 02:55:50 +00:00

mailsender

OpenPGP encryption for Spring Boot mail: discovers recipients' keys via Web Key Directory (WKD) and sends PGP/MIME (RFC 3156), or hands mail to an encrypting relay. See docs/PLAN.md for the design and milestone status.

Requires JDK 25 or newer; the Maven Wrapper (./mvnw) downloads Maven 3.9.11. Built on Spring Boot 4.1.1 and PGPainless 2.0.

./mvnw verify

Modules

Module Contents
mailsender-pgp-core WKD key discovery, caching, pinned keys, certificate validation, OpenPGP encryption, PGP/MIME, OpenPGP and S/MIME signing (no Spring)
mailsender-pgp-spring-boot-autoconfigure mail.pgp.* properties and beans
mailsender-pgp-spring-boot-starter dependency bundle for applications
mailsender-demo sample app that sends test mails

Installation

Releases and snapshots are published to the Gitea Maven registry of the public owner by .gitea/workflows/publish.yml (push to main → -SNAPSHOT, tag v1.2.3 → 1.2.3):

<repositories>
    <repository>
        <id>gitea-public</id>
        <url>https://YOUR-GITEA-HOST/api/packages/public/maven</url>
    </repository>
</repositories>

<dependency>
    <groupId>pro.thinhha.mailsender</groupId>
    <artifactId>mailsender-pgp-spring-boot-starter</artifactId>
    <version>0.1.0</version>
</dependency>

If the packages are not public, add a <server> with id gitea-public and a token with read:package scope to your settings.xml.

Usage

Add pro.thinhha.mailsender:mailsender-pgp-spring-boot-starter, configure spring.mail.* as usual, and send through SecureMailSender instead of JavaMailSender:

SendReport report = secureMailSender.send(message);   // MimeMessage or SimpleMailMessage

Each recipient's key is looked up via WKD (cached) and validated. The message is then sent PGP/MIME-encrypted to recipients with a usable key and in plaintext to the rest (logged as a downgrade), or refused with EncryptionPolicyException if a REQUIRE recipient has no key. SendReport lists the outcome per recipient.

mail:
  pgp:
    mode: BUILT_IN                 # or RELAY: hand plaintext to an encrypting gateway (mail.pgp.relay.*)
    default-policy: OPPORTUNISTIC  # REQUIRE | OPPORTUNISTIC | PLAINTEXT
    mixed-mode: SPLIT              # or FAIL
    recipients:
      "[*@partner.example]": REQUIRE
    sender-keys:                   # your own PUBLIC key, so sent copies stay readable
      "[noreply@example.com]": file:/etc/mail/noreply.pub.asc

Map keys that are addresses need the [...] brackets. See docs/PLAN.md for all options.

  • mail.pgp.protected-headers=true encrypts the real Subject too (RFC 9788); the visible Subject becomes [...].
  • With Micrometer, mail.pgp.recipients{outcome,policy,reason}, mail.pgp.refused, mail.pgp.key.lookup and mail.pgp.key.cache.size are recorded.
  • With Actuator, expose pgpkeys to inspect an address (GET /actuator/pgpkeys/{email}) or evict cached keys (DELETE /actuator/pgpkeys/{email}).

Signing

Outgoing mail can be signed with the From address's OpenPGP key or S/MIME certificate:

mail:
  pgp:
    signing:
      type: OPENPGP          # NONE (default) | OPENPGP | SMIME
      required: false        # true: refuse mail from an address without a signing key
      openpgp:
        "[noreply@example.com]":
          key: file:/etc/mail/noreply.sec.asc     # secret key
          passphrase: ${SIGNING_PASSPHRASE}
      smime:
        "[noreply@example.com]":
          keystore: file:/etc/mail/noreply.p12    # PKCS#12: private key + certificate chain
          password: ${SMIME_PASSWORD}
Plaintext copies Encrypted copies
OPENPGP PGP/MIME multipart/signed (RFC 3156) signed inside the encryption
SMIME S/MIME multipart/signed (RFC 8551) S/MIME-signed, then PGP/MIME-encrypted

Keys are checked at startup: the key/certificate must belong to the address, be able to sign and be currently valid, and the passphrase must work. SendReport.signature() says how a message was signed. In RELAY mode the message is signed before it is handed to the gateway. Do not enable mail.smtp.allow8bitmime together with signing: re-encoding the body in transit invalidates signatures.

Demo app

The demo app sends DEMO_COUNT test mails (default: 1 to test@thinhha.pro) through SecureMailSender and logs the outcome per recipient: PGP/MIME-encrypted (RFC 3156) when a valid WKD key is found, otherwise plaintext. Set MAIL_PGP_MODE=RELAY and RELAY_HOST/RELAY_PORT/RELAY_USERNAME/RELAY_PASSWORD to send via a gateway.

./mvnw -q install -DskipTests
SMTP_HOST=smtp.example.com SMTP_PORT=587 \
SMTP_USERNAME=me@example.com SMTP_PASSWORD='app-password' \
DEMO_TO=test@thinhha.pro DEMO_COUNT=1 \
java -jar mailsender-demo/target/mailsender-demo-0.1.0-SNAPSHOT.jar
Variable Default Meaning
SMTP_HOST / SMTP_PORT localhost / 587 SMTP server
SMTP_USERNAME / SMTP_PASSWORD empty SMTP credentials
SMTP_AUTH / SMTP_STARTTLS true / true set to false for a local test server
DEMO_TO test@thinhha.pro recipient
DEMO_FROM SMTP_USERNAME sender address
DEMO_COUNT 1 number of mails
S
Description
No description provided
Readme AGPL-3.0
232 KiB
0 Stars 1 Watchers 0 Forks
2026-10-15 09:54:26 +00:00
Languages
Java 100%