Skip to content

Commit

Permalink
Preload email files during greenmail start (issue #250)
Browse files Browse the repository at this point in the history
Next step : Enable GreenMail standalone / container
  • Loading branch information
marcelmay committed Oct 21, 2023
1 parent 178b4f6 commit beaaad7
Show file tree
Hide file tree
Showing 15 changed files with 344 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
* text eol=lf
*.eml text eol=crlf

*.gif binary
*.ico binary
*.png binary
*.xcf binary

8 changes: 6 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ <h1>GreenMail</h1>
<a class="nav-item nav-link nav-pills" href="#features">Features</a>
<nav id="nav_features" class="nav nav-pills flex-column collapse" data-parent="#sidebar">
<a class="nav-item nav-link ml-3" href="#features-api">API</a>
<a class="nav-item nav-link ml-3" href="#features-preload">Preloading emails</a>
<a class="nav-item nav-link ml-3" href="#features-dsn">Delivery status notifcation</a>
</nav>
<a class="nav-item nav-link nav-pills" href="#faq">FAQ</a>
<a class="nav-item nav-link nav-pills" href="#download" data-toggle="collapse"
Expand Down Expand Up @@ -228,7 +230,7 @@ <h4>Test Your Retrieving Code</h4>
IMAP by responding like a standard compliant POP3 or IMAP server. Support for POP3S and
IMAPS (SSL) is also enabled.
</li>
<li>Messages can be placed directly in users mailboxes or by using SMTP.</li>
<li>Messages can be placed directly in users mailboxes, by using SMTP or <a href="https://github.com/greenmail-mail-test/greenmail/blob/master/greenmail-core/src/test/java/com/icegreen/greenmail/util/PreLoadEmailsTest.java">preloaded from file system</a>.</li>
<li>GreenMail ships with helper classes for sending and retrieving. See the <a href="https://www.javadoc.io/doc/com.icegreen/greenmail/latest/com/icegreen/greenmail/util/Retriever.html">javadocs</a> for the <a
href="https://github.com/greenmail-mail-test/greenmail/blob/master/greenmail-core/src/main/java/com/icegreen/greenmail/util/Retriever.java">Retriever.java</a>
class
Expand Down Expand Up @@ -1067,8 +1069,10 @@ <h2 id="features-api">API</h2>
alt="GreenMail OpenAPI UI invoking new user request"/>
<figcaption class="figure-caption text-end">GreenMail OpenAPI UI invoking new user request.</figcaption>
</figure>
<h2 id="features-preload">Preloading emails from filesysem</h2>
<p>You can <a href="https://github.com/greenmail-mail-test/greenmail/blob/master/greenmail-core/src/test/java/com/icegreen/greenmail/util/PreLoadEmailsTest.java">preload</a> user/folder/emails from filesystem</p>

<h2>Delivery Status Notification (DSN)</h2>
<h2 id="features-dsn">Delivery Status Notification (DSN)</h2>
<p>You can provide custom DSN behavior by implementing
<a href="https://github.com/greenmail-mail-test/greenmail/blob/master/greenmail-core/src/main/java/com/icegreen/greenmail/user/MessageDeliveryHandler.java">MessageDeliveryHandler.java</a>.
See <a href="https://github.com/greenmail-mail-test/greenmail/blob/master/greenmail-core/src/test/java/com/icegreen/greenmail/examples/ExampleUndeliverableTest.java">ExampleUndeliverableTest.java</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import com.icegreen.greenmail.user.UserManager;

import jakarta.mail.internet.MimeMessage;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Properties;

/**
Expand Down Expand Up @@ -161,4 +164,32 @@ public interface GreenMailOperations {
* @throws FolderException on error
*/
void purgeEmailFromAllMailboxes() throws FolderException;

/**
* Loads emails from given path.
*
* <ul>
* <li>
* Expected structure in provided path, containing EML (rfc0822) mail files
* <p>Pattern: <pre>&lt;EMAIL&gt; / &lt;FOLDER*&gt; / &lt;*.eml&gt;</pre></p>
* <p>Example:</p>
* <pre>
* ├── bar@localhost (directory)
* │ └── INBOX (directory)
* │ └── test-5.eml (file)
* └── foo@localhost (directory)
* ├── Drafts (directory)
* │ └── draft.eml (file)
* </pre>
* </li>
* <li>Creates user of given email if missing (by convention, with email as login and password)</li>
* <li>Creates intermediate mail folder if missing</li>
* </ul>
*
* @param path base path with email structure
* @throws IOException on IO error
* @throws FolderException if e.g. fails to create intermediate folder
* @since 2.1-alpha-3 / 2.0.1 / 1.6.15
*/
GreenMailOperations loadEmails(Path path) throws IOException, FolderException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ public void unsubscribe(GreenMailUser user, String mailboxName)
private String getQualifiedMailboxName(GreenMailUser user, String mailboxName) {
String userNamespace = user.getQualifiedMailboxName();

if ("INBOX".equalsIgnoreCase(mailboxName)) {
if(ImapConstants.INBOX_NAME.equalsIgnoreCase(mailboxName)) {
return USER_NAMESPACE + HIERARCHY_DELIMITER + userNamespace +
HIERARCHY_DELIMITER + INBOX_NAME;
}
Expand Down
128 changes: 108 additions & 20 deletions greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMail.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,35 @@
package com.icegreen.greenmail.util;

import com.icegreen.greenmail.Managers;
import com.icegreen.greenmail.base.GreenMailOperations;
import com.icegreen.greenmail.configuration.ConfiguredGreenMail;
import com.icegreen.greenmail.configuration.GreenMailConfiguration;
import com.icegreen.greenmail.imap.ImapConstants;
import com.icegreen.greenmail.imap.ImapHostManager;
import com.icegreen.greenmail.imap.ImapServer;
import com.icegreen.greenmail.pop3.Pop3Server;
import com.icegreen.greenmail.server.AbstractServer;
import com.icegreen.greenmail.server.BuildInfo;
import com.icegreen.greenmail.smtp.SmtpServer;
import com.icegreen.greenmail.store.FolderException;
import com.icegreen.greenmail.store.InMemoryStore;
import com.icegreen.greenmail.store.MailFolder;
import com.icegreen.greenmail.store.StoredMessage;
import com.icegreen.greenmail.store.*;
import com.icegreen.greenmail.user.GreenMailUser;
import com.icegreen.greenmail.user.UserException;
import com.icegreen.greenmail.user.UserManager;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.internet.MimeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Utility class that manages a greenmail server with support for multiple protocols
Expand Down Expand Up @@ -86,7 +92,7 @@ private void init() {
}

services.clear();
services.putAll( createServices(config, managers) );
services.putAll(createServices(config, managers));
}

@Override
Expand Down Expand Up @@ -114,9 +120,9 @@ public synchronized void start() {
for (AbstractServer service : servers) {
if (!service.isRunning()) {
throw new IllegalStateException("Could not start mail server " + service
+ ", try to set server startup timeout > " + service.getServerSetup().getServerStartupTimeout()
+ " via " + ServerSetup.class.getSimpleName() + ".setServerStartupTimeout(timeoutInMs) or " +
"-Dgreenmail.startup.timeout");
+ ", try to set server startup timeout > " + service.getServerSetup().getServerStartupTimeout()
+ " via " + ServerSetup.class.getSimpleName() + ".setServerStartupTimeout(timeoutInMs) or " +
"-Dgreenmail.startup.timeout");
}
}

Expand Down Expand Up @@ -213,17 +219,17 @@ public UserManager getUserManager() {
public boolean waitForIncomingEmail(long timeout, int emailCount) {
final CountDownLatch waitObject = getManagers().getSmtpManager().createAndAddNewWaitObject(emailCount);
final long endTime = System.currentTimeMillis() + timeout;
while (waitObject.getCount() > 0) {
final long waitTime = endTime - System.currentTimeMillis();
if (waitTime < 0L) {
return waitObject.getCount() == 0;
}
try {
waitObject.await(waitTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// Continue loop, in case of premature interruption
}
while (waitObject.getCount() > 0) {
final long waitTime = endTime - System.currentTimeMillis();
if (waitTime < 0L) {
return waitObject.getCount() == 0;
}
try {
waitObject.await(waitTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// Continue loop, in case of premature interruption
}
}
return waitObject.getCount() == 0;
}

Expand Down Expand Up @@ -322,4 +328,86 @@ public boolean isRunning() {
}
return !services.isEmpty();
}

@Override
public GreenMailOperations loadEmails(Path sourceDirectory) throws IOException, FolderException {
// <SOURCE DIR> / <EMAIL> / <FOLDER*> / <*.eml>
if (!Files.isDirectory(sourceDirectory)) {
throw new IllegalArgumentException("Expected directory: " + sourceDirectory.toAbsolutePath());
}
int sourceNameCount = sourceDirectory.toAbsolutePath().getNameCount();

SmtpServer smtpServer = (null != getSmtp() ? getSmtp() : getSmtps());
if (null == smtpServer) {
throw new IllegalStateException("Requires enabled SMTP(S)");
}
final Session session = smtpServer.createSession();
final UserManager userManager = getUserManager();
final ImapHostManager imapHostManager = getManagers().getImapHostManager();
final Store store = imapHostManager.getStore();

try (final Stream<Path> pathStream = Files.walk(sourceDirectory)) {
for (Path emailPath : pathStream
.filter(path -> !path.equals(sourceDirectory)) // Skip base dir
.map(Path::toAbsolutePath)
.collect(Collectors.toList())) {
loadEmail(sourceDirectory, emailPath, sourceNameCount, userManager, store, imapHostManager, session);
}
}

return this;
}

private void loadEmail(Path sourceDirectory, Path emailPath, int sourceNameCount, UserManager userManager,
Store store, ImapHostManager imapHostManager, Session session)
throws FolderException {
int emailPathNameCount = emailPath.getNameCount();
if (emailPathNameCount - sourceNameCount < 1) {
throw new IllegalArgumentException(
"Expected <USER*> / <FOLDER*> (e.g. INBOX, Drafts, ...) / <*.eml> below " + sourceDirectory + " for " + emailPath);
}

// Extract email as first folder
String email = emailPath.getName(sourceNameCount).toString();
GreenMailUser user = userManager.getUserByEmail(email);
if (null == user) {
try {
user = userManager.createUser(email, email, email);
} catch (UserException e) {
throw new IllegalStateException("Can not create user for email " + email, e);
}
}

// Extract and optionally create intermediate folders
MailFolder folder = null;
folder = store.getMailbox(getUserBaseMailboxName(imapHostManager, user));
for (int i = sourceNameCount + 1; i < emailPathNameCount - 1; i++) {
String namePart = emailPath.getName(i).toString();
MailFolder child = store.getMailbox(folder, namePart);
if (null == child) {
child = store.createMailbox(folder, namePart, true);
}
folder = child;
}

if (Files.isRegularFile(emailPath) && emailPath.toString().endsWith(".eml")) {
try (InputStream source = Files.newInputStream(emailPath)) {
final MimeMessage loadedMsg = new MimeMessage(session, source);
if (log.isDebugEnabled()) {
log.debug("Loading email for {} from {} ...", user.getEmail(), emailPath);
}
folder.store(loadedMsg);
} catch (Exception e) {
throw new IllegalArgumentException("Can not load email " + emailPath, e);
}
}
}

private String getUserBaseMailboxName(ImapHostManager imapHostManager, GreenMailUser user) throws FolderException {
String inbox = imapHostManager.getInbox(user).getFullName();
if (!inbox.toUpperCase().endsWith(ImapConstants.INBOX_NAME)) {
throw new IllegalStateException("Mail folder '" + inbox + "' is not expected " + ImapConstants.INBOX_NAME + " folder");
}
return inbox.substring(0, inbox.length() - ImapConstants.INBOX_NAME.length());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.icegreen.greenmail.util;

import com.icegreen.greenmail.Managers;
import com.icegreen.greenmail.base.GreenMailOperations;
import com.icegreen.greenmail.configuration.ConfiguredGreenMail;
import com.icegreen.greenmail.imap.ImapServer;
import com.icegreen.greenmail.pop3.Pop3Server;
Expand All @@ -10,6 +11,9 @@
import com.icegreen.greenmail.user.UserManager;

import jakarta.mail.internet.MimeMessage;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Properties;

/**
Expand Down Expand Up @@ -118,6 +122,11 @@ public void purgeEmailFromAllMailboxes() throws FolderException {
getGreenMail().purgeEmailFromAllMailboxes();
}

@Override
public GreenMailOperations loadEmails(Path path) throws FolderException, IOException {
return getGreenMail().loadEmails(path);
}

/**
* @return Greenmail instance provided by child class
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.icegreen.greenmail.examples;

import com.icegreen.greenmail.imap.ImapConstants;
import com.icegreen.greenmail.imap.ImapHostManager;
import com.icegreen.greenmail.junit.GreenMailRule;
import com.icegreen.greenmail.user.GreenMailUser;
Expand All @@ -15,14 +16,19 @@
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Example for loading Emails from EML file.
* <p>
* For preloading an existing directory structure, check out {@link com.icegreen.greenmail.util.PreLoadEmailsTest}
*/
public class ExamplePreloadMailFromFsTest {
@Rule
public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP);
public static final String EML_FILE_NAME = ExamplePreloadMailFromFsTest.class.getName() + ".eml";
public static final String EML_FILE_NAME = ExamplePreloadMailFromFsTest.class.getSimpleName() + ".eml";

@Test
public void testPreloadMailFromFs() throws Exception {
Expand All @@ -34,17 +40,19 @@ public void testPreloadMailFromFs() throws Exception {
msg.setFrom("bar@localhost");
msg.setSubject("Hello");
msg.setText("Test message saved as eml (electronic mail format, aka internet message format)");
try (FileOutputStream os = new FileOutputStream(EML_FILE_NAME)) {

final Path emlFile = Files.createTempDirectory("tmp").resolve(EML_FILE_NAME);
try (FileOutputStream os = new FileOutputStream(emlFile.toString())) {
msg.writeTo(os);
}

// Load msg from file system
final ImapHostManager imapHostManager = greenMail.getManagers().getImapHostManager();
final UserManager userManager = greenMail.getManagers().getUserManager();
final GreenMailUser user = userManager.createUser("foo@localhost", "foo-login", "secret");
try (InputStream source = Files.newInputStream(Paths.get(EML_FILE_NAME))) {
try (InputStream source = Files.newInputStream(emlFile)) {
final MimeMessage loadedMsg = new MimeMessage(session, source);
imapHostManager.getFolder(user, "INBOX").store(loadedMsg);
imapHostManager.getFolder(user, ImapConstants.INBOX_NAME).store(loadedMsg);
}

// Verify
Expand Down
Loading

0 comments on commit beaaad7

Please sign in to comment.