From ad00290f891eaeb5c1f0951c50cef14be64ee3df Mon Sep 17 00:00:00 2001 From: Jonas Mayr Date: Thu, 27 Aug 2026 16:33:32 +0200 Subject: [PATCH 1/5] add new default AttachmentDataSource and add migrate it if necessary --- .../EmailServiceConfiguration.java | 13 ++- .../application/JdbcAttachmentDataSource.java | 99 +++++++++++++++++++ .../UnavailableAttachmentDataSource.java | 25 ----- .../JdbcAttachmentDataSourceTest.java | 94 ++++++++++++++++++ 4 files changed, 203 insertions(+), 28 deletions(-) create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java delete mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSourceTest.java diff --git a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java index 0c4bc36..129150f 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java @@ -9,10 +9,10 @@ import it.aboutbits.springboot.emailservice.lib.application.EmailMapper; import it.aboutbits.springboot.emailservice.lib.application.EmailMapperImpl; import it.aboutbits.springboot.emailservice.lib.application.EmailServiceMigrator; +import it.aboutbits.springboot.emailservice.lib.application.JdbcAttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.application.ManageEmail; import it.aboutbits.springboot.emailservice.lib.application.QueryEmail; import it.aboutbits.springboot.emailservice.lib.application.SendScheduledEmails; -import it.aboutbits.springboot.emailservice.lib.application.UnavailableAttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; import org.jspecify.annotations.NullMarked; import org.springframework.beans.factory.annotation.Value; @@ -96,7 +96,14 @@ public CleanupAttachmentFiles cleanupAttachments( @Bean @ConditionalOnMissingBean(AttachmentDataSource.class) - public AttachmentDataSource attachmentDataSource() { - return new UnavailableAttachmentDataSource(); + public JdbcAttachmentDataSource attachmentDataSource( + JdbcTemplate jdbcTemplate, + @Value("${aboutbits.emailservice.migrations.enabled:true}") boolean migrationsEnabled + ) { + var dataSource = new JdbcAttachmentDataSource(jdbcTemplate); + if (migrationsEnabled) { + dataSource.migrate(); + } + return dataSource; } } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java new file mode 100644 index 0000000..de2b6cb --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java @@ -0,0 +1,99 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; +import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.NullMarked; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +@Slf4j +@NullMarked +public class JdbcAttachmentDataSource implements AttachmentDataSource { + private final JdbcTemplate jdbcTemplate; + + public JdbcAttachmentDataSource(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void migrate() { + log.info("EmailService: running attachment payload DB migrations..."); + + jdbcTemplate.execute( + //@formatter:off + """ + + create table if not exists email_service_attachment_payloads + ( + id bigint generated by default as identity + primary key, + payload bytea not null, + created_at timestamp with time zone default now() not null + ); + """ + //@formatter:on + ); + + log.info("EmailService: attachment payload migrations done!"); + } + + @Override + public InputStream getAttachmentPayload(long fileReference) throws AttachmentException { + try { + var payload = jdbcTemplate.queryForObject( + "select payload from email_service_attachment_payloads where id = ?", + byte[].class, + fileReference + ); + if (payload == null) { + throw new AttachmentException("attachment payload not found: " + fileReference); + } + return new ByteArrayInputStream(payload); + } catch (EmptyResultDataAccessException e) { + throw new AttachmentException("attachment payload not found: " + fileReference, e); + } catch (DataAccessException e) { + throw new AttachmentException("failed to load attachment payload: " + fileReference, e); + } + } + + @Override + public long storeAttachmentPayload(InputStream payload) throws AttachmentException { + final byte[] bytes; + try (payload) { + bytes = payload.readAllBytes(); + } catch (IOException e) { + throw new AttachmentException("failed to read attachment payload", e); + } + + try { + var id = jdbcTemplate.queryForObject( + "insert into email_service_attachment_payloads (payload) values (?) returning id", + Long.class, + (Object) bytes + ); + if (id == null) { + throw new AttachmentException("failed to store attachment payload"); + } + return id; + } catch (DataAccessException e) { + throw new AttachmentException("failed to store attachment payload", e); + } + } + + @Override + public void releaseAttachment(long fileReference) throws AttachmentException { + try { + jdbcTemplate.update( + "delete from email_service_attachment_payloads where id = ?", + fileReference + ); + } catch (DataAccessException e) { + throw new AttachmentException("failed to release attachment payload: " + fileReference, e); + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java deleted file mode 100644 index f2ba789..0000000 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java +++ /dev/null @@ -1,25 +0,0 @@ -package it.aboutbits.springboot.emailservice.lib.application; - -import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; -import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; -import org.jspecify.annotations.NullMarked; - -import java.io.InputStream; - -@NullMarked -public final class UnavailableAttachmentDataSource implements AttachmentDataSource { - @Override - public InputStream getAttachmentPayload(long fileReference) throws AttachmentException { - throw new AttachmentException("attachments not available"); - } - - @Override - public long storeAttachmentPayload(InputStream payload) throws AttachmentException { - throw new AttachmentException("attachments not available"); - } - - @Override - public void releaseAttachment(long fileReference) throws AttachmentException { - throw new AttachmentException("attachments not available"); - } -} diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSourceTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSourceTest.java new file mode 100644 index 0000000..ee9d4f5 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSourceTest.java @@ -0,0 +1,94 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; +import it.aboutbits.springboot.emailservice.support.database.WithPostgres; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.ByteArrayInputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@SpringBootTest +@WithPostgres +@NullMarked +class JdbcAttachmentDataSourceTest { + @Autowired + AttachmentDataSource attachmentDataSource; + + @Test + void defaultAttachmentDataSource_shouldBeJdbcBased() { + assertThat(attachmentDataSource).isInstanceOf(JdbcAttachmentDataSource.class); + } + + @Test + void givenPayload_store_shouldBeReadableAgain() throws Exception { + var payload = new byte[]{1, 2, 3, 4, 5}; + + var fileReference = attachmentDataSource.storeAttachmentPayload(new ByteArrayInputStream(payload)); + + try (var stored = attachmentDataSource.getAttachmentPayload(fileReference)) { + assertThat(stored.readAllBytes()).isEqualTo(payload); + } + } + + @Test + void givenPayload_store_shouldClosePayloadStream() throws Exception { + var payload = new TrackingInputStream(new byte[]{1, 2, 3}); + + attachmentDataSource.storeAttachmentPayload(payload); + + assertThat(payload.closed).isTrue(); + } + + @Test + void givenMultiplePayloads_store_shouldReturnDistinctReferences() throws Exception { + var first = attachmentDataSource.storeAttachmentPayload(new ByteArrayInputStream(new byte[]{1})); + var second = attachmentDataSource.storeAttachmentPayload(new ByteArrayInputStream(new byte[]{2})); + + assertThat(first).isNotEqualTo(second); + } + + @Test + void givenUnknownReference_get_shouldFail() { + assertThatExceptionOfType(AttachmentException.class).isThrownBy( + () -> attachmentDataSource.getAttachmentPayload(1L) + ); + } + + @Test + void givenStoredPayload_release_shouldRemoveIt() throws Exception { + var fileReference = attachmentDataSource.storeAttachmentPayload(new ByteArrayInputStream(new byte[]{1, 2, 3})); + + attachmentDataSource.releaseAttachment(fileReference); + + assertThatExceptionOfType(AttachmentException.class).isThrownBy( + () -> attachmentDataSource.getAttachmentPayload(fileReference) + ); + } + + @Test + void givenUnknownReference_release_shouldBeIdempotent() { + assertThatCode( + () -> attachmentDataSource.releaseAttachment(1L) + ).doesNotThrowAnyException(); + } + + private static final class TrackingInputStream extends ByteArrayInputStream { + private boolean closed = false; + + private TrackingInputStream(byte[] buf) { + super(buf); + } + + @Override + public void close() { + closed = true; + } + } +} From e0a231fcafc1d88b8f9955acc5a85b575906b49c Mon Sep 17 00:00:00 2001 From: Jonas Mayr Date: Thu, 27 Aug 2026 16:33:48 +0200 Subject: [PATCH 2/5] update readme --- readme.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 5b94346..3f7a1e3 100644 --- a/readme.md +++ b/readme.md @@ -17,8 +17,13 @@ Add the mailer service to the classpath by adding the following maven dependency ### Attachments -If you want to use attachments, you will have to create a bean implementing this interface: [AttachmentDataSource.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2FAttachmentDataSource.java) -This step is optional. +Attachments work out of the box. The library ships a default that stores attachment payloads in the +lib-owned table `email_service_attachment_payloads`. The table is created automatically the first time the default is +used, unless `aboutbits.emailservice.migrations.enabled` is set to `false`. + +If you want to store payloads somewhere else (e.g. S3), define your own bean implementing this +interface: [AttachmentDataSource.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2FAttachmentDataSource.java) +The default then backs off and its table is never created. #### Inline (CID) attachments From 5b9979fe6b2d2262fe7aba811cf8c5831186e578 Mon Sep 17 00:00:00 2001 From: Jonas Mayr Date: Fri, 28 Aug 2026 08:55:38 +0200 Subject: [PATCH 3/5] make default attachment data source opt-in --- pom.xml | 5 ++-- readme.md | 19 +++++++++----- .../EmailServiceConfiguration.java | 21 +++------------- .../application/JdbcAttachmentDataSource.java | 3 ++- .../UnavailableAttachmentDataSource.java | 25 +++++++++++++++++++ .../JdbcAttachmentDataSourceTest.java | 13 ++++++---- 6 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java diff --git a/pom.xml b/pom.xml index 3c3e55d..214ad04 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -11,7 +12,7 @@ it.aboutbits.springboot emailservice - 1.2.0 + BUILD-SNAPSHOT Spring Boot Email Service diff --git a/readme.md b/readme.md index 3f7a1e3..33ff539 100644 --- a/readme.md +++ b/readme.md @@ -17,13 +17,20 @@ Add the mailer service to the classpath by adding the following maven dependency ### Attachments -Attachments work out of the box. The library ships a default that stores attachment payloads in the -lib-owned table `email_service_attachment_payloads`. The table is created automatically the first time the default is -used, unless `aboutbits.emailservice.migrations.enabled` is set to `false`. +If you want to use attachments, you will have to define a bean implementing this interface: [AttachmentDataSource.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2FAttachmentDataSource.java) +This step is optional. Where the payloads are stored is a decision of the application: you can provide your own +implementation (e.g. S3), or register the ready-made `JdbcAttachmentDataSource` shipped with the library, which stores +payloads in the lib-owned table `email_service_attachment_payloads`: -If you want to store payloads somewhere else (e.g. S3), define your own bean implementing this -interface: [AttachmentDataSource.java](src%2Fmain%2Fjava%2Fit%2Faboutbits%2Fspringboot%2Femailservice%2Flib%2FAttachmentDataSource.java) -The default then backs off and its table is never created. +```java + +@Bean +public AttachmentDataSource attachmentDataSource(JdbcTemplate jdbcTemplate) { + return new JdbcAttachmentDataSource(jdbcTemplate); +} +``` + +The `JdbcAttachmentDataSource` creates its table itself on construction, so no further setup is needed. #### Inline (CID) attachments diff --git a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java index 129150f..f72adbf 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/EmailServiceConfiguration.java @@ -9,15 +9,15 @@ import it.aboutbits.springboot.emailservice.lib.application.EmailMapper; import it.aboutbits.springboot.emailservice.lib.application.EmailMapperImpl; import it.aboutbits.springboot.emailservice.lib.application.EmailServiceMigrator; -import it.aboutbits.springboot.emailservice.lib.application.JdbcAttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.application.ManageEmail; import it.aboutbits.springboot.emailservice.lib.application.QueryEmail; import it.aboutbits.springboot.emailservice.lib.application.SendScheduledEmails; +import it.aboutbits.springboot.emailservice.lib.application.UnavailableAttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.jpa.EmailRepository; import org.jspecify.annotations.NullMarked; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigurationPackage; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; @@ -55,7 +55,7 @@ public QueryEmail queryEmail(EmailRepository emailRepository, EmailMapper emailM public ManageEmail manageEmail( EmailRepository emailRepository, JavaMailSender javaMailSender, - AttachmentDataSource attachmentDataSource, + ObjectProvider attachmentDataSource, EmailMapper emailMapper, @Value("${aboutbits.emailservice.scheduling.max-attempts:3}") int maxAttempts, @Value("${aboutbits.emailservice.scheduling.interval:30000}") long schedulerIntervalMillis, @@ -64,7 +64,7 @@ public ManageEmail manageEmail( return new ManageEmail( emailRepository, javaMailSender, - attachmentDataSource, + attachmentDataSource.getIfAvailable(UnavailableAttachmentDataSource::new), emailMapper, maxAttempts, Duration.ofMillis(schedulerIntervalMillis), @@ -93,17 +93,4 @@ public CleanupAttachmentFiles cleanupAttachments( ) { return new CleanupAttachmentFiles(queryEmail, manageEmail, callbacks, stuckCleanupRecoveryThreshold); } - - @Bean - @ConditionalOnMissingBean(AttachmentDataSource.class) - public JdbcAttachmentDataSource attachmentDataSource( - JdbcTemplate jdbcTemplate, - @Value("${aboutbits.emailservice.migrations.enabled:true}") boolean migrationsEnabled - ) { - var dataSource = new JdbcAttachmentDataSource(jdbcTemplate); - if (migrationsEnabled) { - dataSource.migrate(); - } - return dataSource; - } } diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java index de2b6cb..334025b 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java @@ -19,9 +19,10 @@ public class JdbcAttachmentDataSource implements AttachmentDataSource { public JdbcAttachmentDataSource(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; + migrate(); } - public void migrate() { + private void migrate() { log.info("EmailService: running attachment payload DB migrations..."); jdbcTemplate.execute( diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java new file mode 100644 index 0000000..f2ba789 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java @@ -0,0 +1,25 @@ +package it.aboutbits.springboot.emailservice.lib.application; + +import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; +import org.jspecify.annotations.NullMarked; + +import java.io.InputStream; + +@NullMarked +public final class UnavailableAttachmentDataSource implements AttachmentDataSource { + @Override + public InputStream getAttachmentPayload(long fileReference) throws AttachmentException { + throw new AttachmentException("attachments not available"); + } + + @Override + public long storeAttachmentPayload(InputStream payload) throws AttachmentException { + throw new AttachmentException("attachments not available"); + } + + @Override + public void releaseAttachment(long fileReference) throws AttachmentException { + throw new AttachmentException("attachments not available"); + } +} diff --git a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSourceTest.java b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSourceTest.java index ee9d4f5..2b59a2c 100644 --- a/src/test/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSourceTest.java +++ b/src/test/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSourceTest.java @@ -1,12 +1,13 @@ package it.aboutbits.springboot.emailservice.lib.application; -import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; import it.aboutbits.springboot.emailservice.support.database.WithPostgres; import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; import java.io.ByteArrayInputStream; @@ -19,11 +20,13 @@ @NullMarked class JdbcAttachmentDataSourceTest { @Autowired - AttachmentDataSource attachmentDataSource; + JdbcTemplate jdbcTemplate; - @Test - void defaultAttachmentDataSource_shouldBeJdbcBased() { - assertThat(attachmentDataSource).isInstanceOf(JdbcAttachmentDataSource.class); + JdbcAttachmentDataSource attachmentDataSource; + + @BeforeEach + void setup() { + attachmentDataSource = new JdbcAttachmentDataSource(jdbcTemplate); } @Test From 40d49460ffa0b3ff7739ddc1e70aaa1f2837e8c3 Mon Sep 17 00:00:00 2001 From: Jonas Mayr Date: Fri, 28 Aug 2026 09:29:11 +0200 Subject: [PATCH 4/5] add more docs and add test for config --- pom.xml | 5 +- readme.md | 2 - .../lib/AttachmentDataSource.java | 7 ++ .../EmailServiceConfigurationTest.java | 89 +++++++++++++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/test/java/it/aboutbits/springboot/emailservice/EmailServiceConfigurationTest.java diff --git a/pom.xml b/pom.xml index 214ad04..3c3e55d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 @@ -12,7 +11,7 @@ it.aboutbits.springboot emailservice - BUILD-SNAPSHOT + 1.2.0 Spring Boot Email Service diff --git a/readme.md b/readme.md index 33ff539..ba57be5 100644 --- a/readme.md +++ b/readme.md @@ -30,8 +30,6 @@ public AttachmentDataSource attachmentDataSource(JdbcTemplate jdbcTemplate) { } ``` -The `JdbcAttachmentDataSource` creates its table itself on construction, so no further setup is needed. - #### Inline (CID) attachments To embed an attachment inline, set a `contentId` on the attachment and reference it in the `htmlBody` via the `cid:` scheme. diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java index 813e1e4..720c8fe 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/AttachmentDataSource.java @@ -9,6 +9,13 @@ public interface AttachmentDataSource { InputStream getAttachmentPayload(long fileReference) throws AttachmentException; + /** + * Stores the payload and returns the reference used to read it back later. + * The implementation takes ownership of the stream and must close it. + * + * @param payload the attachment payload, closed by the implementation + * @return the reference to pass to {@link #getAttachmentPayload(long)} + */ long storeAttachmentPayload(InputStream payload) throws AttachmentException; void releaseAttachment(long fileReference) throws AttachmentException; diff --git a/src/test/java/it/aboutbits/springboot/emailservice/EmailServiceConfigurationTest.java b/src/test/java/it/aboutbits/springboot/emailservice/EmailServiceConfigurationTest.java new file mode 100644 index 0000000..bb50571 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/emailservice/EmailServiceConfigurationTest.java @@ -0,0 +1,89 @@ +package it.aboutbits.springboot.emailservice; + +import it.aboutbits.springboot.emailservice.lib.AttachmentDataSource; +import it.aboutbits.springboot.emailservice.lib.EmailState; +import it.aboutbits.springboot.emailservice.lib.application.EmailParameter; +import it.aboutbits.springboot.emailservice.lib.application.ManageEmail; +import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; +import it.aboutbits.springboot.emailservice.lib.exception.EmailException; +import it.aboutbits.springboot.emailservice.support.database.WithPostgres; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +import java.io.ByteArrayInputStream; +import java.time.OffsetDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@SpringBootTest +@WithPostgres +@NullMarked +class EmailServiceConfigurationTest { + @Autowired + ApplicationContext applicationContext; + + @Autowired + ManageEmail manageEmail; + + @Test + void givenNoAttachmentDataSourceBean_context_shouldStart() { + assertThat(applicationContext.getBeanNamesForType(AttachmentDataSource.class)).isEmpty(); + assertThat(manageEmail).isNotNull(); + } + + @Test + void givenNoAttachmentDataSourceBean_scheduleWithoutAttachment_shouldSucceed() throws EmailException { + var result = manageEmail.schedule(getValidParameterWithoutAttachment()); + + assertThat(result.id()).isPositive(); + assertThat(result.state()).isEqualTo(EmailState.PENDING); + } + + @Test + void givenNoAttachmentDataSourceBean_scheduleWithAttachment_shouldFail() { + var parameter = getValidParameterWithAttachment(); + + assertThatExceptionOfType(EmailException.class).isThrownBy( + () -> manageEmail.schedule(parameter) + ).withCauseInstanceOf(AttachmentException.class); + } + + private static EmailParameter getValidParameterWithoutAttachment() { + return EmailParameter.builder() + .scheduledAt(OffsetDateTime.now()) + .email(EmailParameter.Email.builder() + .subject("Example email subject") + .textBody("Email body") + .htmlBody("

Html email body

") + .recipient("person1@example.com") + .fromAddress("somebody@aboutbits.it") + .fromName("somebody") + .build() + ).build(); + } + + private static EmailParameter getValidParameterWithAttachment() { + return EmailParameter.builder() + .scheduledAt(OffsetDateTime.now()) + .email(EmailParameter.Email.builder() + .subject("Example email subject") + .textBody("Email body") + .htmlBody("

Html email body

") + .recipient("person1@example.com") + .attachment( + EmailParameter.Email.Attachment.builder() + .contentType("image/png") + .fileName("x.png") + .payload(new ByteArrayInputStream(new byte[0])) + .build() + ) + .fromAddress("somebody@aboutbits.it") + .fromName("somebody") + .build() + ).build(); + } +} From be96e8634806d40510a98963a5b89dd88b90ac92 Mon Sep 17 00:00:00 2001 From: Jonas Mayr Date: Tue, 1 Sep 2026 07:50:02 +0200 Subject: [PATCH 5/5] requested changes --- readme.md | 3 +++ .../lib/application/JdbcAttachmentDataSource.java | 8 +++++++- .../lib/application/UnavailableAttachmentDataSource.java | 7 ++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index ba57be5..3f7aa28 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,9 @@ public AttachmentDataSource attachmentDataSource(JdbcTemplate jdbcTemplate) { } ``` +By default, `JdbcAttachmentDataSource` creates its table on startup. If you prefer to manage the table yourself +(e.g. via Liquibase), disable the built-in migration using the constructor-flag and create the table in your own migrations. + #### Inline (CID) attachments To embed an attachment inline, set a `contentId` on the attachment and reference it in the `htmlBody` via the `cid:` scheme. diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java index 334025b..e161fdb 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/JdbcAttachmentDataSource.java @@ -18,8 +18,14 @@ public class JdbcAttachmentDataSource implements AttachmentDataSource { private final JdbcTemplate jdbcTemplate; public JdbcAttachmentDataSource(JdbcTemplate jdbcTemplate) { + this(jdbcTemplate, true); + } + + public JdbcAttachmentDataSource(JdbcTemplate jdbcTemplate, boolean runMigrations) { this.jdbcTemplate = jdbcTemplate; - migrate(); + if (runMigrations) { + migrate(); + } } private void migrate() { diff --git a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java index f2ba789..5120223 100644 --- a/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java +++ b/src/main/java/it/aboutbits/springboot/emailservice/lib/application/UnavailableAttachmentDataSource.java @@ -4,6 +4,7 @@ import it.aboutbits.springboot.emailservice.lib.exception.AttachmentException; import org.jspecify.annotations.NullMarked; +import java.io.IOException; import java.io.InputStream; @NullMarked @@ -15,7 +16,11 @@ public InputStream getAttachmentPayload(long fileReference) throws AttachmentExc @Override public long storeAttachmentPayload(InputStream payload) throws AttachmentException { - throw new AttachmentException("attachments not available"); + try (payload) { + throw new AttachmentException("attachments not available"); + } catch (IOException e) { + throw new AttachmentException("attachments not available", e); + } } @Override