diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6b9d196 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ + +.idea/ +*.iml +target/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1f019f --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Post Office + +Bring your mail to the post office. It will be stored and a postal worker will deliver it. +Because of the always recurring task in every project to store a mail and send it out with a worker I extracted this as a +module. + +This module is auto configured and depends on spring mail. + +Currently only MongoDB is supported. For other databases implement the `MailStorage` interface. + +## Installation + +[![Release](https://jitpack.io/v/nschwalbe/postoffice.svg?style=flat-square)](https://jitpack.io/#nschwalbe/postoffice) + +The module is build by jitpack and can be downloaded with maven or gradle. + +Add the jitpack repository: + +```xml + + + jitpack.io + https://jitpack.io + + +``` + +And dependency: + +```xml + + com.github.nschwalbe + postoffice + develop-SNAPSHOT + +``` + +For more information like gradle see here: + + +## Configuration +### Mail Server +Configure at least the `spring.mail.host` property. For more information see the spring mail configuration. + + + +### Thread Pool +The default `ThreadPoolTaskScheduler` is used which comes by default with only one thread. +To configure the scheduler define a configuration class which implements `SchedulingConfigurer`. +For example: + +```java +@Configuration +@EnableScheduling +public class TaskExecutionConfig implements SchedulingConfigurer { + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(3); + threadPoolTaskScheduler.initialize(); + taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); + } +} +``` + +## Usage + +Define a Mailer class in your project and inject the `MailService`. + +```java +@Component +public class MyMailer { + + private final PostOffice postOffice; + private final TemplateEngine templateEngine; + private final MessageSource messageSource; + + @Autowired + public MyMailer(PostOffice postOffice, TemplateEngine templateEngine, Environment environment, MessageSource messageSource) { + this.postOffice = postOffice; + this.templateEngine = templateEngine; + this.messageSource = messageSource; + } + + public void sendMail() { + + Context ctx = createContext(); + + try { + String subject = createSubject(); + String content = createContent(ctx); + + MimeMessage mimeMessage = postOffice.createMimeMessage(subject, from, to, content, true); + + postOffice.postMail(mimeMessage); + + } catch (Exception e) { + log.error("Could not create mail!", e); + } + } +} +``` + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c731112 --- /dev/null +++ b/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + de.nschwalbe + postoffice + 0.1.3 + + + org.springframework.boot + spring-boot-starter-parent + 1.5.2.RELEASE + + + + + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-data-mongodb + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + junit + junit + + + + + com.icegreen + greenmail + 1.5.3 + test + + + junit + junit + + + + + + org.testng + testng + 6.11 + test + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + verify + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + attach-javadocs + + jar + + + + + + + + diff --git a/src/main/java/de/nschwalbe/postoffice/MailAddress.java b/src/main/java/de/nschwalbe/postoffice/MailAddress.java new file mode 100644 index 0000000..f3ea3e9 --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/MailAddress.java @@ -0,0 +1,36 @@ +package de.nschwalbe.postoffice; + +import java.util.Objects; + +/** + * A mail address. + * + * @author Nathanael Schwalbe + * @since 14.04.2017 + */ +public class MailAddress { + + private final String address; + private final String personal; + + private MailAddress(String address, String personal) { + this.address = Objects.requireNonNull(address); + this.personal = personal; + } + + public static MailAddress of(String address, String personal) { + return new MailAddress(address, personal); + } + + public static MailAddress of(String address) { + return new MailAddress(address, null); + } + + public String getAddress() { + return address; + } + + public String getPersonal() { + return personal; + } +} diff --git a/src/main/java/de/nschwalbe/postoffice/MailProcessState.java b/src/main/java/de/nschwalbe/postoffice/MailProcessState.java new file mode 100644 index 0000000..67bd180 --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/MailProcessState.java @@ -0,0 +1,13 @@ +package de.nschwalbe.postoffice; + +/** + * Defines processing state of an mail. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +public enum MailProcessState { + + NOT_SENT, IN_PROGRESS, FAILED, SENT + +} diff --git a/src/main/java/de/nschwalbe/postoffice/MailStorage.java b/src/main/java/de/nschwalbe/postoffice/MailStorage.java new file mode 100644 index 0000000..72c977e --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/MailStorage.java @@ -0,0 +1,22 @@ +package de.nschwalbe.postoffice; + +import java.util.List; + +/** + * Persists a mail to be send by a scheduled task. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +public interface MailStorage { + + PersistedMail create(byte[] mimeMessageContent); + + List findNotSentIds(); + + PersistedMail findNotSentAndStartProgress(String mailId); + + void delete(String id); + + void update(PersistedMail mail); +} diff --git a/src/main/java/de/nschwalbe/postoffice/PersistedMail.java b/src/main/java/de/nschwalbe/postoffice/PersistedMail.java new file mode 100644 index 0000000..28e6fc7 --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/PersistedMail.java @@ -0,0 +1,25 @@ +package de.nschwalbe.postoffice; + +import java.time.LocalDateTime; + +/** + * Defines a mail to be stored in a database. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +public interface PersistedMail { + + String getId(); + + LocalDateTime getCreatedDate(); + LocalDateTime getLastModifiedDate(); + + byte[] getMimeMessageContent(); + + MailProcessState getState(); + void setState(MailProcessState state); + + String getErrorMessage(); + void setErrorMessage(String message); +} diff --git a/src/main/java/de/nschwalbe/postoffice/PostOffice.java b/src/main/java/de/nschwalbe/postoffice/PostOffice.java new file mode 100644 index 0000000..14c320c --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/PostOffice.java @@ -0,0 +1,150 @@ +package de.nschwalbe.postoffice; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.mail.MessagingException; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; + +/** + * Service facade for sending mails. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +public class PostOffice { + + private static final Logger log = LoggerFactory.getLogger(PostOffice.class); + + private final MailStorage mailStorage; + private final JavaMailSender mailSender; + + public PostOffice(MailStorage mailStorage, JavaMailSender mailSender) { + this.mailStorage = mailStorage; + this.mailSender = mailSender; + } + + /** + * Creates a mail, stores it and sends it out later. This method returns immediately and does not wait for the mail server. + * + * @param subject the mail subject. + * @param from the sender address. + * @param to the recipient. + * @param content the mail body. + * @param isHtml true if mail body is html, if it is plain text set to false. + * @return the persisted mail + * @throws MessagingException if message creation failed due to some error. + */ + public PersistedMail postMail(String subject, MailAddress from, MailAddress to, String content, boolean isHtml) throws MessagingException { + MimeMessage mimeMessage = createMimeMessage(subject, from, Collections.singletonList(to), content, isHtml); + return postMail(mimeMessage); + } + + /** + * Creates a mail, stores it and sends it out later. This method returns immediately and does not wait for the mail server. + * + * @param subject the mail subject. + * @param from the sender address. + * @param to the recipient. + * @param html the mail body html part. + * @param text the mail body text part. + * @return the persisted mail. + * @throws MessagingException if message creation failed due to some error. + */ + public PersistedMail postMail(String subject, MailAddress from, MailAddress to, String html, String text) throws MessagingException { + MimeMessage mimeMessage = createMimeMessage(subject, from, Collections.singletonList(to), html, text); + return postMail(mimeMessage); + } + + /** + * Creates a mail, stores it and sends it out later. This method returns immediately and does not wait for the mail server. + * + * @param subject the mail subject. + * @param from the sender address. + * @param to the recipients. + * @param content the mail body. + * @param isHtml true if mail body is html, if it is plain text set to false. + * @return the persisted mail + * @throws MessagingException if message creation failed due to some error. + */ + public PersistedMail postMail(String subject, MailAddress from, List to, String content, boolean isHtml) throws MessagingException { + MimeMessage mimeMessage = createMimeMessage(subject, from, to, content, isHtml); + return postMail(mimeMessage); + } + + /** + * Creates a mail, stores it and sends it out later. This method returns immediately and does not wait for the mail server. + * + * @param subject the mail subject. + * @param from the sender address. + * @param to the recipients. + * @param html the mail body html part. + * @param text the mail body text part. + * @return the persisted mail. + * @throws MessagingException if message creation failed due to some error. + */ + public PersistedMail postMail(String subject, MailAddress from, List to, String html, String text) throws MessagingException { + MimeMessage mimeMessage = createMimeMessage(subject, from, to, html, text); + return postMail(mimeMessage); + } + + private PersistedMail postMail(MimeMessage mimeMessage) throws MessagingException { + + byte[] content; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + mimeMessage.writeTo(out); + content = out.toByteArray(); + } catch (Exception e) { + throw new MessagingException("Error reading mime message content!", e); + } + + return mailStorage.create(content); + } + + private MimeMessage createMimeMessage(String subject, MailAddress from, List to, String content, boolean isHtml) throws MessagingException { + MimeMessageHelper messageHelper = createMimeMessageHelper(subject, from, to); + messageHelper.setText(content, isHtml); + return messageHelper.getMimeMessage(); + } + + private MimeMessage createMimeMessage(String subject, MailAddress from, List to, String html, String text) throws MessagingException { + MimeMessageHelper messageHelper = createMimeMessageHelper(subject, from, to); + messageHelper.setText(text, html); + return messageHelper.getMimeMessage(); + } + + private MimeMessageHelper createMimeMessageHelper(String subject, MailAddress from, List to) throws MessagingException { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper message = new MimeMessageHelper(mimeMessage, "UTF-8"); + message.setSubject(subject); + try { + message.setFrom(from.getAddress(), from.getPersonal()); + } catch (UnsupportedEncodingException e) { + throw new MessagingException("Error creating From-Address", e); + } + + InternetAddress[] internetAddresses = to.stream() + .map(mailAddress -> { + try { + return new InternetAddress(mailAddress.getAddress(), mailAddress.getPersonal(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + log.error("This should never happen!", e); + return null; + } + }) + .filter(Objects::nonNull) + .toArray(InternetAddress[]::new); + message.setTo(internetAddresses); + return message; + } +} diff --git a/src/main/java/de/nschwalbe/postoffice/PostOfficeConfiguration.java b/src/main/java/de/nschwalbe/postoffice/PostOfficeConfiguration.java new file mode 100644 index 0000000..9040e51 --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/PostOfficeConfiguration.java @@ -0,0 +1,55 @@ +package de.nschwalbe.postoffice; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.config.TriggerTask; + +/** + * Configuration of the mailing module. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +@Configuration +public class PostOfficeConfiguration { + + @Autowired + private JavaMailSender javaMailSender; + + @Autowired + private Environment env; + + @Bean + public PostOffice postOffice(MailStorage mailStorage) { + return new PostOffice(mailStorage, javaMailSender); + } + + @Bean("sendMailTask") + public SendMailTaskFactory sendMailTaskFactory(MailStorage mailStorage) { + return new SendMailTaskFactory(mailStorage, javaMailSender, env); + } + + @EnableScheduling + @Configuration + static class SchedulerConfiguration implements SchedulingConfigurer { + + @Autowired + private TriggerTask sendMailTask; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + try { + taskRegistrar.addTriggerTask(sendMailTask); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + } +} diff --git a/src/main/java/de/nschwalbe/postoffice/SendMailTaskFactory.java b/src/main/java/de/nschwalbe/postoffice/SendMailTaskFactory.java new file mode 100644 index 0000000..970fc08 --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/SendMailTaskFactory.java @@ -0,0 +1,174 @@ +package de.nschwalbe.postoffice; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.List; + +import javax.mail.internet.MimeMessage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.core.env.Environment; +import org.springframework.mail.MailAuthenticationException; +import org.springframework.mail.MailException; +import org.springframework.mail.MailSendException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.scheduling.config.TriggerTask; + +/** + * Mail sending job and trigger. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +class SendMailTaskFactory extends AbstractFactoryBean { + + private final MailStorage mailStorage; + private final JavaMailSender javaMailSender; + private final Environment env; + + SendMailTaskFactory(MailStorage mailStorage, JavaMailSender javaMailSender, Environment env) { + this.mailStorage = mailStorage; + this.javaMailSender = javaMailSender; + this.env = env; + } + + @Override + public Class getObjectType() { + return TriggerTask.class; + } + + @Override + protected TriggerTask createInstance() throws Exception { + SendMailTrigger trigger = new SendMailTrigger(env.getProperty("postoffice.worker.delay", Integer.class, 10)); + SendMailTask task = new SendMailTask(mailStorage, javaMailSender, trigger); + return new TriggerTask(task, trigger); + } + + static class SendMailTrigger implements Trigger { + + private int defaultDelay; + private int delay; + + + SendMailTrigger(int delay) { + this.defaultDelay = delay; + this.delay = delay; + } + + // try again in 30s, then wait 60s, then wait 90s ... + void increaseDelay() { + delay += 30; + + // max delay between tries is 5 minutes + if (delay > 300) { + delay = 300; + } + } + + + void resetDelay() { + delay = defaultDelay; + } + + @Override + public Date nextExecutionTime(TriggerContext triggerContext) { + + Date lastRun = triggerContext.lastCompletionTime(); + ZonedDateTime nextRun; + + if (lastRun == null) { + nextRun = ZonedDateTime.now().plusSeconds(delay); + } + else { + nextRun = ZonedDateTime.ofInstant(lastRun.toInstant(), ZoneId.systemDefault()).plusSeconds(delay); + } + + return Date.from(nextRun.toInstant()); + } + } + + static class SendMailTask implements Runnable { + + private Logger log = LoggerFactory.getLogger(SendMailTask.class); + + private final MailStorage mailStorage; + private final JavaMailSender javaMailSender; + private final SendMailTrigger trigger; + + SendMailTask(MailStorage mailStorage, JavaMailSender javaMailSender, SendMailTrigger trigger) { + this.mailStorage = mailStorage; + this.javaMailSender = javaMailSender; + this.trigger = trigger; + } + + public void run() { + + log.trace("Start mail shipping ..."); + + List mailIds = mailStorage.findNotSentIds(); + + if (!mailIds.isEmpty()) { + log.debug("Sending {} mails.", mailIds.size()); + } + + for (String mailId : mailIds) { + + PersistedMail mail = mailStorage.findNotSentAndStartProgress(mailId); + + if (mail == null) { + log.warn("Mail with id {} is not found any more. Skipping it.", mailId); + continue; + } + + if (mail.getState() != MailProcessState.IN_PROGRESS) { + log.warn("Mail with id {} was not set to in_progress but is {}. Skipping it", mailId, mail.getState()); + continue; + } + + try (InputStream in = new ByteArrayInputStream(mail.getMimeMessageContent())) { + + MimeMessage mimeMessage = javaMailSender.createMimeMessage(in); + javaMailSender.send(mimeMessage); + trigger.resetDelay(); + updateMail(mail, MailProcessState.SENT, null); + + } catch (IOException e) { + log.error("Could not create MimeMessage from blob. Email could not be sent!", e); + updateMail(mail, MailProcessState.FAILED, e.getMessage()); + + } catch (MailAuthenticationException e) { + log.error("Could not send mail because of incorrect credentials. __Fix email configuration!__ Trying to send this email again later.", e); + updateMail(mail, MailProcessState.NOT_SENT, e.getMessage()); + trigger.increaseDelay(); + break; // every other mail sending will also be failing + + } catch (MailSendException e) { + log.error("Could not send mail because of a network error! Trying again later.", e); + updateMail(mail, MailProcessState.NOT_SENT, e.getMessage()); + trigger.increaseDelay(); + break; // every other mail sending will also be failing + + } catch (MailException e) { + log.error("Email cannot be send and is thrown away!", e); + updateMail(mail, MailProcessState.FAILED, e.getMessage()); + } + } + + log.trace("Finished mail shipping."); + } + + private void updateMail(PersistedMail mail, MailProcessState state, String message) { + mail.setState(state); + mail.setErrorMessage(message); + mailStorage.update(mail); + } + } +} diff --git a/src/main/java/de/nschwalbe/postoffice/mongodb/MailDocument.java b/src/main/java/de/nschwalbe/postoffice/mongodb/MailDocument.java new file mode 100644 index 0000000..7bda2ae --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/mongodb/MailDocument.java @@ -0,0 +1,83 @@ +package de.nschwalbe.postoffice.mongodb; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import de.nschwalbe.postoffice.MailProcessState; +import de.nschwalbe.postoffice.PersistedMail; + +/** + * A mail persisted with mongodb. Mails will be automatically removed after 3 days if send or not. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +@Document(collection = "mails") +class MailDocument implements PersistedMail { + + @Id + private String id; + + @Indexed(expireAfterSeconds = 259200) + @CreatedDate + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime lastModifiedDate; + + private MailProcessState state = MailProcessState.NOT_SENT; + private String errorMessage; + + private final byte[] mimeMessageContent; + + @PersistenceConstructor + MailDocument(byte[] mimeMessageContent) { + this.mimeMessageContent = mimeMessageContent; + } + + @Override + public String getId() { + return id; + } + + @Override + public LocalDateTime getLastModifiedDate() { + return lastModifiedDate; + } + + @Override + public MailProcessState getState() { + return state; + } + + @Override + public void setState(MailProcessState state) { + this.state = state; + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + + @Override + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public byte[] getMimeMessageContent() { + return mimeMessageContent; + } + + @Override + public LocalDateTime getCreatedDate() { + return createdDate; + } +} diff --git a/src/main/java/de/nschwalbe/postoffice/mongodb/MongoMailStorage.java b/src/main/java/de/nschwalbe/postoffice/mongodb/MongoMailStorage.java new file mode 100644 index 0000000..3a10bd3 --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/mongodb/MongoMailStorage.java @@ -0,0 +1,70 @@ +package de.nschwalbe.postoffice.mongodb; + +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.mongodb.core.FindAndModifyOptions; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import de.nschwalbe.postoffice.MailProcessState; +import de.nschwalbe.postoffice.MailStorage; +import de.nschwalbe.postoffice.PersistedMail; + +/** + * Stores mails in mongodb. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +class MongoMailStorage implements MailStorage { + + private final MongoOperations mongoOperations; + + MongoMailStorage(MongoOperations mongoOperations) { + this.mongoOperations = mongoOperations; + } + + @Override + public MailDocument create(byte[] mimeMessageContent) { + MailDocument mailDocument = new MailDocument(mimeMessageContent); + mongoOperations.save(mailDocument); + return mailDocument; + } + + @Override + public List findNotSentIds() { + + Query query = query(where("state").is(MailProcessState.NOT_SENT)); + query.fields().include("_id"); + + List mailDocuments = mongoOperations.find(query, MailDocument.class); + + return mailDocuments.stream().map(MailDocument::getId).collect(Collectors.toList()); + } + + @Override + public MailDocument findNotSentAndStartProgress(String mailId) { + + Query query = query(where("_id").is(mailId).and("state").is(MailProcessState.NOT_SENT)); + Update update = Update.update("state", MailProcessState.IN_PROGRESS); + return mongoOperations.findAndModify(query, update, FindAndModifyOptions.options().returnNew(true), MailDocument.class); + } + + @Override + public void delete(String id) { + mongoOperations.remove(query(where("_id").is(id)), MailDocument.class); + } + + @Override + public void update(PersistedMail mail) { + if (mail.getId() == null) { + throw new IllegalArgumentException("Cannot update mail because it is not persisted yet."); + } + mongoOperations.save(mail); + } +} diff --git a/src/main/java/de/nschwalbe/postoffice/mongodb/MongoMailStorageConfiguration.java b/src/main/java/de/nschwalbe/postoffice/mongodb/MongoMailStorageConfiguration.java new file mode 100644 index 0000000..5f3c9dd --- /dev/null +++ b/src/main/java/de/nschwalbe/postoffice/mongodb/MongoMailStorageConfiguration.java @@ -0,0 +1,26 @@ +package de.nschwalbe.postoffice.mongodb; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.MongoOperations; + +import de.nschwalbe.postoffice.MailStorage; + +/** + * Configuration for the mongo storage. + * + * @author Nathanael Schwalbe + * @since 06.04.2017 + */ +@ConditionalOnMissingBean(MailStorage.class) +@ConditionalOnClass(MongoOperations.class) +@Configuration +public class MongoMailStorageConfiguration { + + @Bean + public MongoMailStorage mongoMailStorage(MongoOperations mongoOperations) { + return new MongoMailStorage(mongoOperations); + } +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..272b3d0 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +de.nschwalbe.postoffice.PostOfficeConfiguration,\ +de.nschwalbe.postoffice.mongodb.MongoMailStorageConfiguration diff --git a/src/test/java/de/nschwalbe/postoffice/mongodb/MongoMailStorageTest.java b/src/test/java/de/nschwalbe/postoffice/mongodb/MongoMailStorageTest.java new file mode 100644 index 0000000..bbac5db --- /dev/null +++ b/src/test/java/de/nschwalbe/postoffice/mongodb/MongoMailStorageTest.java @@ -0,0 +1,83 @@ +package de.nschwalbe.postoffice.mongodb; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.Charset; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.Test; + +import de.nschwalbe.postoffice.PostOfficeConfiguration; +import de.nschwalbe.postoffice.MailProcessState; + +/** + * Integration tests for the mongo mail storage. + * + * @author Nathanael Schwalbe + * @since 05.04.2017 + */ +@SpringBootTest({"spring.mail.host=localhost", "spring.mail.port=2525"}) +public class MongoMailStorageTest extends AbstractTestNGSpringContextTests { + + @Autowired + private MongoMailStorage mongoMailStorage; + + @Autowired + private MongoOperations mongoOperations; + + private String mailId; + + @Test + public void shouldInsertMail() { + + String message = "Email Content"; + MailDocument mailDocument = mongoMailStorage.create(message.getBytes(Charset.forName("UTF-8"))); + + assertThat(mailDocument).isNotNull(); + assertThat(mailDocument.getId()).isNotNull(); + assertThat(mailDocument.getState()).isEqualTo(MailProcessState.NOT_SENT); + + this.mailId = mailDocument.getId(); + boolean exists = mongoOperations.exists(Query.query(Criteria.where("_id").is(mailId)), MailDocument.class); + assertThat(exists).isTrue(); + } + + @Test(dependsOnMethods = "shouldInsertMail") + public void shouldFindNotSentId() { + + List ids = mongoMailStorage.findNotSentIds(); + + assertThat(ids).hasSize(1); + assertThat(ids).contains(mailId); + } + + @Test(dependsOnMethods = "shouldFindNotSentId") + public void shouldFindAndStartProgress() { + + MailDocument mailDocument = mongoMailStorage.findNotSentAndStartProgress(mailId); + + assertThat(mailDocument).isNotNull(); + assertThat(mailDocument.getState()).isEqualTo(MailProcessState.IN_PROGRESS); + } + + @Test(dependsOnMethods = "shouldFindAndStartProgress") + public void shouldDelete() { + + mongoMailStorage.delete(mailId); + boolean exists = mongoOperations.exists(Query.query(Criteria.where("_id").is(mailId)), MailDocument.class); + assertThat(exists).isFalse(); + } + + @SpringBootApplication + @Import({ PostOfficeConfiguration.class}) + static class TestConfiguration { + } +}