mirror of
https://github.com/sneakybugmaster/mailsender.git
synced 2026-11-10 19:48:49 +00:00
Scaffold PGP mail modules with WKD key discovery and demo app
- Maven multi-module build on JDK 27 and Spring Boot 4.1.1 (BOM import) - core: WKD discovery (advanced -> direct fallback), HTTP fetcher with timeouts, no redirects and a size limit, Caffeine cache with per-outcome TTLs capped at key expiry, pinned keys, composite chain - autoconfigure: mail.pgp.* properties and discovery beans - demo: sends test mails (plaintext for now) and reports the WKD result - docs/PLAN.md: design, decisions and milestones Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G71TQ5jDETVwdUJxGAwaiT
This commit is contained in:
35 files changed
+1885
No files matched your search
+14
@@ -0,0 +1,14 @@
|
||||
package io.github.sneakybugmaster.mailsender.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
public class DemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.github.sneakybugmaster.mailsender.demo;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
|
||||
/**
|
||||
* @param to recipient of the test mails
|
||||
* @param from sender address; most SMTP providers require it to match the authenticated account
|
||||
* @param count number of test mails to send
|
||||
*/
|
||||
@ConfigurationProperties("demo")
|
||||
public record DemoProperties(
|
||||
@DefaultValue("test@thinhha.pro") String to,
|
||||
String from,
|
||||
@DefaultValue("1") int count) {
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package io.github.sneakybugmaster.mailsender.demo;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.github.sneakybugmaster.mailsender.pgp.discovery.DiscoveryResult;
|
||||
import io.github.sneakybugmaster.mailsender.pgp.discovery.KeyDiscovery;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
|
||||
/**
|
||||
* Looks up the recipient's key via WKD, then sends {@code demo.count} test mails describing what was found.
|
||||
* <p>
|
||||
* Encryption is not wired in yet (milestones 3–6), so the mails go out in plaintext.
|
||||
*/
|
||||
@Component
|
||||
class TestMailRunner implements ApplicationRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TestMailRunner.class);
|
||||
|
||||
private final DemoProperties properties;
|
||||
private final KeyDiscovery keyDiscovery;
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
TestMailRunner(DemoProperties properties, KeyDiscovery keyDiscovery, JavaMailSender mailSender) {
|
||||
this.properties = properties;
|
||||
this.keyDiscovery = keyDiscovery;
|
||||
this.mailSender = mailSender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws MessagingException {
|
||||
DiscoveryResult discovery = keyDiscovery.discover(properties.to());
|
||||
String summary = describe(discovery);
|
||||
log.info("Key discovery for {}: {}", properties.to(), summary);
|
||||
|
||||
for (int i = 1; i <= properties.count(); i++) {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, "UTF-8");
|
||||
if (properties.from() != null) {
|
||||
helper.setFrom(properties.from());
|
||||
}
|
||||
helper.setTo(properties.to());
|
||||
helper.setSubject("mailsender test %d/%d".formatted(i, properties.count()));
|
||||
helper.setText("""
|
||||
Test mail %d of %d from the mailsender demo, sent %s.
|
||||
|
||||
Key discovery for %s: %s
|
||||
|
||||
This message was sent in plaintext: PGP/MIME encryption is not implemented yet.
|
||||
""".formatted(i, properties.count(), OffsetDateTime.now(), properties.to(), summary));
|
||||
mailSender.send(message);
|
||||
log.info("Sent test mail {}/{} to {}", i, properties.count(), properties.to());
|
||||
}
|
||||
}
|
||||
|
||||
private static String describe(DiscoveryResult result) {
|
||||
return switch (result) {
|
||||
case DiscoveryResult.Found found -> "found via " + found.source() + ", fingerprint(s) "
|
||||
+ found.certificates().stream()
|
||||
.map(c -> HexFormat.of().withUpperCase().formatHex(c.getFingerprint()))
|
||||
.collect(Collectors.joining(", "));
|
||||
case DiscoveryResult.NotFound ignored -> "no key published";
|
||||
case DiscoveryResult.Unreachable u -> "lookup failed (" + u.reason() + ")";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
spring:
|
||||
main:
|
||||
web-application-type: none
|
||||
mail:
|
||||
host: ${SMTP_HOST:localhost}
|
||||
port: ${SMTP_PORT:587}
|
||||
username: ${SMTP_USERNAME:}
|
||||
password: ${SMTP_PASSWORD:}
|
||||
properties:
|
||||
mail.smtp.auth: ${SMTP_AUTH:true}
|
||||
mail.smtp.starttls.enable: ${SMTP_STARTTLS:true}
|
||||
mail.smtp.starttls.required: ${SMTP_STARTTLS:true}
|
||||
mail.smtp.connectiontimeout: 10000
|
||||
mail.smtp.timeout: 10000
|
||||
|
||||
demo:
|
||||
to: ${DEMO_TO:test@thinhha.pro}
|
||||
from: ${DEMO_FROM:${SMTP_USERNAME:}}
|
||||
count: ${DEMO_COUNT:1}
|
||||
|
||||
logging:
|
||||
level:
|
||||
io.github.sneakybugmaster.mailsender: DEBUG
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.github.sneakybugmaster.mailsender.demo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
|
||||
import com.icegreen.greenmail.util.GreenMail;
|
||||
import com.icegreen.greenmail.util.GreenMailUtil;
|
||||
import com.icegreen.greenmail.util.ServerSetupTest;
|
||||
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
|
||||
@SpringBootTest(properties = {
|
||||
"spring.mail.properties.mail.smtp.auth=false",
|
||||
"spring.mail.properties.mail.smtp.starttls.enable=false",
|
||||
"spring.mail.properties.mail.smtp.starttls.required=false",
|
||||
"spring.mail.username=",
|
||||
"demo.from=demo@mailsender.test",
|
||||
"demo.count=2",
|
||||
// keep the test offline: no real WKD lookup
|
||||
"mail.pgp.wkd.enabled=false"
|
||||
})
|
||||
class TestMailRunnerTest {
|
||||
|
||||
// Started before the Spring context, because the runner sends while the context starts.
|
||||
static GreenMail greenMail;
|
||||
|
||||
@BeforeAll
|
||||
static void startSmtp() {
|
||||
greenMail = new GreenMail(ServerSetupTest.SMTP.dynamicPort());
|
||||
greenMail.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopSmtp() {
|
||||
greenMail.stop();
|
||||
}
|
||||
|
||||
@DynamicPropertySource
|
||||
static void smtp(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.mail.host", () -> "127.0.0.1");
|
||||
registry.add("spring.mail.port", () -> greenMail.getSmtp().getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendsTestMailsToConfiguredRecipient() throws Exception {
|
||||
MimeMessage[] received = greenMail.getReceivedMessages();
|
||||
|
||||
assertThat(received).hasSize(2);
|
||||
assertThat(received[0].getAllRecipients()).extracting(Object::toString).containsExactly("test@thinhha.pro");
|
||||
assertThat(received[0].getSubject()).isEqualTo("mailsender test 1/2");
|
||||
assertThat(GreenMailUtil.getBody(received[0])).contains("no key published");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user