mirror of
https://github.com/sneakybugmaster/mailsender.git
synced 2026-11-10 19:48:49 +00:00
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
130 lines
5.2 KiB
Markdown
130 lines
5.2 KiB
Markdown
# 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](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`):
|
|
|
|
```xml
|
|
<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`:
|
|
|
|
```java
|
|
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.
|
|
|
|
```yaml
|
|
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](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:
|
|
|
|
```yaml
|
|
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 |
|