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 {
+ }
+}