Add certificate validation and PGP/MIME encryption; rename to pro.thinhha.mailsender

- validation: CertificateValidator checks revocation, expiry, matching
  user ID, a valid communications encryption subkey and the algorithm
  policy; uses the earlier expiry when PGPainless and Bouncy Castle disagree
- crypto: PgpEncryptor (ASCII armor, hidden recipients for Bcc)
- mime: PgpMimeEncryptor builds RFC 3156 multipart/encrypted from the
  whole CRLF-canonical inner entity, keeping envelope headers in the clear
- autoconfigure: validator and encryptor beans
- demo: encrypts when a valid key is found, plaintext otherwise
- rename groupId and Java packages to pro.thinhha.mailsender

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G71TQ5jDETVwdUJxGAwaiT
This commit is contained in:
Claude committed 2026-10-12 03:20:33 +00:00
1 parent 213df4c34e
commit a788f7a48a
44 files changed
+958 -142

No files matched your search

@@ -1,4 +1,4 @@
package io.github.sneakybugmaster.mailsender.pgp.autoconfigure;
package pro.thinhha.mailsender.pgp.autoconfigure;
import java.io.IOException;
import java.io.InputStream;
@@ -18,19 +18,23 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import io.github.sneakybugmaster.mailsender.pgp.discovery.CachingKeyDiscovery;
import io.github.sneakybugmaster.mailsender.pgp.discovery.Certificates;
import io.github.sneakybugmaster.mailsender.pgp.discovery.CompositeKeyDiscovery;
import io.github.sneakybugmaster.mailsender.pgp.discovery.HostResolver;
import io.github.sneakybugmaster.mailsender.pgp.discovery.HttpWkdFetcher;
import io.github.sneakybugmaster.mailsender.pgp.discovery.KeyDiscovery;
import io.github.sneakybugmaster.mailsender.pgp.discovery.StaticKeyDiscovery;
import io.github.sneakybugmaster.mailsender.pgp.discovery.WkdFetcher;
import io.github.sneakybugmaster.mailsender.pgp.discovery.WkdKeyDiscovery;
import pro.thinhha.mailsender.pgp.crypto.PgpEncryptor;
import pro.thinhha.mailsender.pgp.discovery.CachingKeyDiscovery;
import pro.thinhha.mailsender.pgp.discovery.Certificates;
import pro.thinhha.mailsender.pgp.discovery.CompositeKeyDiscovery;
import pro.thinhha.mailsender.pgp.discovery.HostResolver;
import pro.thinhha.mailsender.pgp.discovery.HttpWkdFetcher;
import pro.thinhha.mailsender.pgp.discovery.KeyDiscovery;
import pro.thinhha.mailsender.pgp.discovery.StaticKeyDiscovery;
import pro.thinhha.mailsender.pgp.discovery.WkdFetcher;
import pro.thinhha.mailsender.pgp.discovery.WkdKeyDiscovery;
import pro.thinhha.mailsender.pgp.mime.PgpMimeEncryptor;
import pro.thinhha.mailsender.pgp.validation.CertificateValidator;
/**
* Key discovery beans: pinned keys first, then WKD behind a cache. The primary {@link KeyDiscovery} bean is the
* combined chain; {@link CachingKeyDiscovery} is exposed separately so applications can evict entries.
* Key discovery (pinned keys first, then WKD behind a cache), certificate validation and PGP/MIME encryption beans.
* The primary {@link KeyDiscovery} bean is the combined chain; {@link CachingKeyDiscovery} is exposed separately so
* applications can evict entries.
*/
@AutoConfiguration
@EnableConfigurationProperties(PgpMailProperties.class)
@@ -75,6 +79,24 @@ public class PgpMailAutoConfiguration {
return new CompositeKeyDiscovery(chain);
}
@Bean
@ConditionalOnMissingBean
CertificateValidator pgpCertificateValidator() {
return new CertificateValidator();
}
@Bean
@ConditionalOnMissingBean
PgpEncryptor pgpEncryptor() {
return new PgpEncryptor();
}
@Bean
@ConditionalOnMissingBean
PgpMimeEncryptor pgpMimeEncryptor(PgpEncryptor encryptor) {
return new PgpMimeEncryptor(encryptor);
}
private static List<OpenPGPCertificate> read(String email, Resource resource) {
try (InputStream in = resource.getInputStream()) {
List<OpenPGPCertificate> certificates = Certificates.parse(in.readAllBytes());
@@ -1,4 +1,4 @@
package io.github.sneakybugmaster.mailsender.pgp.autoconfigure;
package pro.thinhha.mailsender.pgp.autoconfigure;
import java.time.Duration;
import java.util.Map;
@@ -8,7 +8,7 @@ import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.core.io.Resource;
import org.springframework.util.unit.DataSize;
import io.github.sneakybugmaster.mailsender.pgp.discovery.WkdFallback;
import pro.thinhha.mailsender.pgp.discovery.WkdFallback;
/**
* {@code mail.pgp.*} configuration.
@@ -1 +1 @@
io.github.sneakybugmaster.mailsender.pgp.autoconfigure.PgpMailAutoConfiguration
pro.thinhha.mailsender.pgp.autoconfigure.PgpMailAutoConfiguration
@@ -1,4 +1,4 @@
package io.github.sneakybugmaster.mailsender.pgp.autoconfigure;
package pro.thinhha.mailsender.pgp.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
@@ -13,12 +13,14 @@ import org.pgpainless.PGPainless;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import io.github.sneakybugmaster.mailsender.pgp.discovery.CachingKeyDiscovery;
import io.github.sneakybugmaster.mailsender.pgp.discovery.DiscoveryResult;
import io.github.sneakybugmaster.mailsender.pgp.discovery.DiscoverySource;
import io.github.sneakybugmaster.mailsender.pgp.discovery.KeyDiscovery;
import io.github.sneakybugmaster.mailsender.pgp.discovery.WkdFallback;
import io.github.sneakybugmaster.mailsender.pgp.discovery.WkdFetcher;
import pro.thinhha.mailsender.pgp.discovery.CachingKeyDiscovery;
import pro.thinhha.mailsender.pgp.discovery.DiscoveryResult;
import pro.thinhha.mailsender.pgp.discovery.DiscoverySource;
import pro.thinhha.mailsender.pgp.discovery.KeyDiscovery;
import pro.thinhha.mailsender.pgp.discovery.WkdFallback;
import pro.thinhha.mailsender.pgp.discovery.WkdFetcher;
import pro.thinhha.mailsender.pgp.mime.PgpMimeEncryptor;
import pro.thinhha.mailsender.pgp.validation.CertificateValidator;
class PgpMailAutoConfigurationTest {
@@ -31,7 +33,8 @@ class PgpMailAutoConfigurationTest {
@Test
void createsDiscoveryChainWithDefaults() {
runner.run(context -> {
assertThat(context).hasSingleBean(WkdFetcher.class).hasSingleBean(CachingKeyDiscovery.class);
assertThat(context).hasSingleBean(WkdFetcher.class).hasSingleBean(CachingKeyDiscovery.class)
.hasSingleBean(CertificateValidator.class).hasSingleBean(PgpMimeEncryptor.class);
assertThat(context.getBean(KeyDiscovery.class)).isNotInstanceOf(CachingKeyDiscovery.class);
PgpMailProperties properties = context.getBean(PgpMailProperties.class);