diff --git a/.gitattributes b/.gitattributes index 56618f0268..1f596a36e1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,8 @@ * text eol=lf +*.eml text eol=crlf *.gif binary *.ico binary *.png binary *.xcf binary + diff --git a/docs/index.html b/docs/index.html index aadeda0363..328c649977 100644 --- a/docs/index.html +++ b/docs/index.html @@ -97,6 +97,8 @@

GreenMail

Features FAQ Test Your Retrieving Code IMAP by responding like a standard compliant POP3 or IMAP server. Support for POP3S and IMAPS (SSL) is also enabled. -
  • Messages can be placed directly in users mailboxes or by using SMTP.
  • +
  • Messages can be placed directly in users mailboxes, by using SMTP or preloaded from file system.
  • GreenMail ships with helper classes for sending and retrieving. See the javadocs for the Retriever.java class @@ -1067,8 +1069,10 @@

    API

    alt="GreenMail OpenAPI UI invoking new user request"/>
    GreenMail OpenAPI UI invoking new user request.
    +

    Preloading emails from filesysem

    +

    You can preload user/folder/emails from filesystem

    -

    Delivery Status Notification (DSN)

    +

    Delivery Status Notification (DSN)

    You can provide custom DSN behavior by implementing MessageDeliveryHandler.java. See ExampleUndeliverableTest.java diff --git a/greenmail-core/src/main/java/com/icegreen/greenmail/base/GreenMailOperations.java b/greenmail-core/src/main/java/com/icegreen/greenmail/base/GreenMailOperations.java index 8bbf775bf4..03d815e15b 100644 --- a/greenmail-core/src/main/java/com/icegreen/greenmail/base/GreenMailOperations.java +++ b/greenmail-core/src/main/java/com/icegreen/greenmail/base/GreenMailOperations.java @@ -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; /** @@ -161,4 +164,32 @@ public interface GreenMailOperations { * @throws FolderException on error */ void purgeEmailFromAllMailboxes() throws FolderException; + + /** + * Loads emails from given path. + * + *

    + * + * @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; } diff --git a/greenmail-core/src/main/java/com/icegreen/greenmail/imap/ImapHostManagerImpl.java b/greenmail-core/src/main/java/com/icegreen/greenmail/imap/ImapHostManagerImpl.java index 89939c2e26..4650c28346 100644 --- a/greenmail-core/src/main/java/com/icegreen/greenmail/imap/ImapHostManagerImpl.java +++ b/greenmail-core/src/main/java/com/icegreen/greenmail/imap/ImapHostManagerImpl.java @@ -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; } diff --git a/greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMail.java b/greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMail.java index 5ad0452b39..85e2550415 100644 --- a/greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMail.java +++ b/greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMail.java @@ -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 @@ -86,7 +92,7 @@ private void init() { } services.clear(); - services.putAll( createServices(config, managers) ); + services.putAll(createServices(config, managers)); } @Override @@ -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"); } } @@ -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; } @@ -322,4 +328,86 @@ public boolean isRunning() { } return !services.isEmpty(); } + + @Override + public GreenMailOperations loadEmails(Path sourceDirectory) throws IOException, FolderException { + // / / / <*.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 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 / (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()); + } } diff --git a/greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMailProxy.java b/greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMailProxy.java index 7e353283ac..78930cc2e5 100644 --- a/greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMailProxy.java +++ b/greenmail-core/src/main/java/com/icegreen/greenmail/util/GreenMailProxy.java @@ -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; @@ -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; /** @@ -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 */ diff --git a/greenmail-core/src/test/java/com/icegreen/greenmail/examples/ExamplePreloadMailFromFsTest.java b/greenmail-core/src/test/java/com/icegreen/greenmail/examples/ExamplePreloadMailFromFsTest.java index 8d2e773490..ba60c612ea 100644 --- a/greenmail-core/src/test/java/com/icegreen/greenmail/examples/ExamplePreloadMailFromFsTest.java +++ b/greenmail-core/src/test/java/com/icegreen/greenmail/examples/ExamplePreloadMailFromFsTest.java @@ -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; @@ -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. + *

    + * 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 { @@ -34,7 +40,9 @@ 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); } @@ -42,9 +50,9 @@ public void testPreloadMailFromFs() throws Exception { 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 diff --git a/greenmail-core/src/test/java/com/icegreen/greenmail/util/PreLoadEmailsTest.java b/greenmail-core/src/test/java/com/icegreen/greenmail/util/PreLoadEmailsTest.java new file mode 100644 index 0000000000..9dc8a15200 --- /dev/null +++ b/greenmail-core/src/test/java/com/icegreen/greenmail/util/PreLoadEmailsTest.java @@ -0,0 +1,114 @@ +package com.icegreen.greenmail.util; + +import com.icegreen.greenmail.imap.ImapConstants; +import com.icegreen.greenmail.imap.ImapServer; +import com.icegreen.greenmail.junit.GreenMailRule; +import com.icegreen.greenmail.store.FolderException; +import com.icegreen.greenmail.user.GreenMailUser; +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import org.eclipse.angus.mail.imap.IMAPStore; +import org.junit.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.FileSystems; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PreLoadEmailsTest { + @Rule + public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP); + + @Test + public void testPreloadFromDirectory() throws IOException, MessagingException, FolderException { + /* Expected structure: + preload + ├── bar@localhost + │ └── INBOX + │ └── test-5.eml + ├── foo-bar@localhost + │ └── INBOX # Auto-created + ├── drafts@localhost + | ├── Drafts + │ └── INBOX # Auto-created + └── foo@localhost + ├── Drafts + │ └── draft.eml + └── INBOX + ├── f1 + │ ├── f2 + │ │ ├── test-3.eml + │ │ └── test-4.eml + │ └── test-2.eml + └── test-1.eml + */ + final GreenMailUser existingUser = greenMail.setUser("bar@localhost", "bar@localhost", "bar"); + greenMail.loadEmails(FileSystems.getDefault().getPath("test-classes/preload")); + final ImapServer imap = greenMail.getImap(); + try (IMAPStore store = imap.createStore()) { + store.connect("foo@localhost", "foo@localhost"); + + try (Folder inbox = store.getFolder(ImapConstants.INBOX_NAME)) { + inbox.open(Folder.READ_ONLY); + final Message[] messages = inbox.getMessages(); + assertThat(messages).hasSize(1); + assertThat(messages[0].getSubject()).isEqualTo("test-1"); + + try (Folder f1 = inbox.getFolder("f1")) { + f1.open(Folder.READ_ONLY); + final Message[] f1Messages = f1.getMessages(); + assertThat(f1Messages).hasSize(1); + assertThat(f1Messages[0].getSubject()).isEqualTo("test-2"); + + try (Folder f2 = f1.getFolder("f2")) { + f2.open(Folder.READ_ONLY); + final Message[] f2Messages = f2.getMessages(); + assertThat(f2Messages).hasSize(2); + assertThat(f2Messages[0].getSubject()).isIn("test-3", "test-4"); + } + } + } + try (Folder inbox = store.getFolder("Drafts")) { + inbox.open(Folder.READ_ONLY); + final Message[] messages = inbox.getMessages(); + assertThat(messages).hasSize(1); + assertThat(messages[0].getSubject()).isEqualTo("Draft-1"); + } + } + + // Empty folder for user 'foo-bar' + try (IMAPStore store = imap.createStore()) { + store.connect("foo-bar@localhost", "foo-bar@localhost"); + try (Folder inbox = store.getFolder(ImapConstants.INBOX_NAME)) { + inbox.open(Folder.READ_ONLY); + assertThat(inbox.getMessages()).isEmpty(); + } + } + + // Only Drafts folder + try (IMAPStore store = imap.createStore()) { + store.connect("drafts@localhost", "drafts@localhost"); + try (Folder inbox = store.getFolder(ImapConstants.INBOX_NAME)) { // Auto-created + inbox.open(Folder.READ_ONLY); + assertThat(inbox.getMessages()).isEmpty(); + } + try (Folder drafts = store.getFolder("Drafts")) { // From filesystem structure + drafts.open(Folder.READ_ONLY); + assertThat(drafts.getMessages()).isEmpty(); + } + } + + // Pre-created user 'bar' + try (IMAPStore store = imap.createStore()) { + store.connect(existingUser.getLogin(), existingUser.getPassword()); + try (Folder inbox = store.getFolder(ImapConstants.INBOX_NAME)) { + inbox.open(Folder.READ_ONLY); + final Message[] messages = inbox.getMessages(); + assertThat(messages).hasSize(1); + assertThat(messages[0].getSubject()).isEqualTo("test-5"); + } + } + } +} diff --git a/greenmail-core/src/test/resources/preload/bar@localhost/INBOX/test-5.eml b/greenmail-core/src/test/resources/preload/bar@localhost/INBOX/test-5.eml new file mode 100644 index 0000000000..7e8c95ae8c --- /dev/null +++ b/greenmail-core/src/test/resources/preload/bar@localhost/INBOX/test-5.eml @@ -0,0 +1,10 @@ +Date: Sat, 16 Sep 2023 17:10:44 +0200 (CEST) +From: bar@localhost +To: foo@localhost +Message-ID: <1882145060.266.1694877044545@127.0.0.1> +Subject: test-5 +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Test message saved as eml (electronic mail format, aka internet message format) diff --git a/greenmail-core/src/test/resources/preload/foo-bar@localhost/.keepme b/greenmail-core/src/test/resources/preload/foo-bar@localhost/.keepme new file mode 100644 index 0000000000..e69de29bb2 diff --git a/greenmail-core/src/test/resources/preload/foo@localhost/Drafts/draft.eml b/greenmail-core/src/test/resources/preload/foo@localhost/Drafts/draft.eml new file mode 100644 index 0000000000..ca8546fc2a --- /dev/null +++ b/greenmail-core/src/test/resources/preload/foo@localhost/Drafts/draft.eml @@ -0,0 +1,10 @@ +Date: Sat, 16 Sep 2023 17:10:44 +0200 (CEST) +From: foo@localhost +To: bar@localhost +Message-ID: <1882145060.266.1694877044545@127.0.0.1> +Subject: Draft-1 +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Test message saved as eml (electronic mail format, aka internet message format) diff --git a/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/f2/test-3.eml b/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/f2/test-3.eml new file mode 100644 index 0000000000..f811b059c7 --- /dev/null +++ b/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/f2/test-3.eml @@ -0,0 +1,10 @@ +Date: Sat, 16 Sep 2023 17:10:44 +0200 (CEST) +From: bar@localhost +To: foo@localhost +Message-ID: <1882145060.266.1694877044545@127.0.0.1> +Subject: test-3 +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Test message saved as eml (electronic mail format, aka internet message format) diff --git a/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/f2/test-4.eml b/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/f2/test-4.eml new file mode 100644 index 0000000000..ffcfc2b81a --- /dev/null +++ b/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/f2/test-4.eml @@ -0,0 +1,10 @@ +Date: Sat, 16 Sep 2023 17:10:44 +0200 (CEST) +From: bar@localhost +To: foo@localhost +Message-ID: <1882145060.266.1694877044545@127.0.0.1> +Subject: test-4 +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Test message saved as eml (electronic mail format, aka internet message format) diff --git a/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/test-2.eml b/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/test-2.eml new file mode 100644 index 0000000000..fcc481a57d --- /dev/null +++ b/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/f1/test-2.eml @@ -0,0 +1,10 @@ +Date: Sat, 16 Sep 2023 17:10:44 +0200 (CEST) +From: bar@localhost +To: foo@localhost +Message-ID: <1882145060.266.1694877044545@127.0.0.1> +Subject: test-2 +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Test message saved as eml (electronic mail format, aka internet message format) diff --git a/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/test-1.eml b/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/test-1.eml new file mode 100644 index 0000000000..8b957eed90 --- /dev/null +++ b/greenmail-core/src/test/resources/preload/foo@localhost/INBOX/test-1.eml @@ -0,0 +1,10 @@ +Date: Sat, 16 Sep 2023 17:10:44 +0200 (CEST) +From: bar@localhost +To: foo@localhost +Message-ID: <1882145060.266.1694877044545@127.0.0.1> +Subject: test-1 +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Test message saved as eml (electronic mail format, aka internet message format)