Skip to content

Commit

Permalink
#427: Added support for maximum email size validation with a possible…
Browse files Browse the repository at this point in the history
… EmailTooBig exception as result (as cause)
  • Loading branch information
bbottema committed Jan 24, 2023
1 parent 93a2125 commit 8f39f04
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.simplejavamail.api.mailer;

import static java.lang.String.format;

/**
* Thrown when an email (as MimeMessage) is bigger than the maximum allowed size.
*
* @see MailerGenericBuilder#withMaximumEmailSize(int)
*/
public class EmailTooBigException extends RuntimeException {
public EmailTooBigException(final long emailSize, final long maximumEmailSize) {
super(format("Email size of %s bytes exceeds maximum allowed size of %s bytes", emailSize, maximumEmailSize));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ public interface MailerGenericBuilder<T extends MailerGenericBuilder<?>> {
*/
T withEmailOverrides(@NotNull Email emailoverrides);

/**
* Sets a maximum size for emails (as MimeMessage) in bytes. If an email exceeds this size, exception @{@link EmailTooBigException} will be thrown (as the cause).
*
* @param maximumEmailSize Maximum size of an email (as MimeMessage) in bytes.
* @see #clearMaximumEmailSize()
*/
T withMaximumEmailSize(int maximumEmailSize);

/**
* Signs this <em>all emails by default</em> with an <a href="https://tools.ietf.org/html/rfc5751">S/MIME</a> signature, so the receiving client
* can verify whether the email content was tampered with.
Expand Down Expand Up @@ -685,6 +693,13 @@ public interface MailerGenericBuilder<T extends MailerGenericBuilder<?>> {
*/
T clearEmailOverrides();

/**
* Makes the maximum email size <code>null</code>, meaning no size check will be performed.
*
* @see #withMaximumEmailSize(int)
*/
T clearMaximumEmailSize();

/**
* Removes S/MIME signing, so emails won't be signed by default.
*
Expand Down Expand Up @@ -780,6 +795,12 @@ public interface MailerGenericBuilder<T extends MailerGenericBuilder<?>> {
@Nullable
Email getEmailOverrides();

/**
* @see #withMaximumEmailSize(int)
*/
@Nullable
Integer getMaximumEmailSize();

/**
* @see #signByDefaultWithSmime(Pkcs12Config)
* @see #signByDefaultWithSmime(InputStream, String, String, String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
@Getter()
public class EmailGovernance {

public static final EmailGovernance NO_GOVERNANCE = new EmailGovernance(null, null, null, null);
public static final EmailGovernance NO_GOVERNANCE = new EmailGovernance(null, null, null, null, null);

/**
* The effective email validator used for email validation. Can be <code>null</code> if no validation should be done.
Expand Down Expand Up @@ -56,4 +56,10 @@ public class EmailGovernance {
* @see MailerGenericBuilder#withEmailOverrides(Email)
*/
@Nullable private final Email emailOverrides;

/**
* Determines at what size Simple Java Mail should reject a MimeMessage. Useful if you know your SMTP server has a limit.
* @see MailerGenericBuilder#withMaximumEmailSize(int)
*/
@Nullable private final Integer maximumEmailSize;
}
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ public static MimeMessage emailToMimeMessage(@NotNull final Email email, @NotNul
try {
return MimeMessageProducerHelper.produceMimeMessage(
checkNonEmptyArgument(email, "email"),
new EmailGovernance(null, checkNonEmptyArgument(defaultSmimeSigningStore, "defaultSmimeSigningStore"), null, null),
new EmailGovernance(null, checkNonEmptyArgument(defaultSmimeSigningStore, "defaultSmimeSigningStore"), null, null, null),
checkNonEmptyArgument(session, "session"));
} catch (UnsupportedEncodingException | MessagingException e) {
// this should never happen, so we don't acknowledge this exception (and simply bubble up)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import org.simplejavamail.internal.modules.SMIMEModule;
import org.simplejavamail.internal.util.MiscUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -22,8 +22,8 @@ public class ModuleLoader {
private static final Map<Class, Object> LOADED_MODULES = new HashMap<>();

// used from junit tests
private static final Collection<Class> FORCED_DISABLED_MODULES = new ArrayList<>();
private static final Collection<Class> FORCED_RECHECK_MODULES = new ArrayList<>();
private static final Set<Class> FORCED_DISABLED_MODULES = new HashSet<>();
private static final Set<Class> FORCED_RECHECK_MODULES = new HashSet<>();

public static AuthenticatedSocksModule loadAuthenticatedSocksModule() {
if (!LOADED_MODULES.containsKey(AuthenticatedSocksModule.class)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
/**
* This exception is used to communicate errors during the sending of email.
*/
@SuppressWarnings("serial")
class MailerException extends MailException {

static final String ERROR_READING_SMIME_FROM_INPUTSTREAM = "Was unable to read S/MIME data from input stream";
static final String ERROR_READING_FROM_FILE = "Error reading from file: %s";
static final String MISSING_OAUTH2_TOKEN = "TransportStrategy is OAUTH2 but no OAUTH2 token provided as password";
static final String INVALID_PROXY_SLL_COMBINATION = "Proxy is not supported for SSL connections (this is a limitation by the underlying JavaMail framework)";
static final String ERROR_CONNECTING_SMTP_SERVER = "Was unable to connect to SMTP server";
static final String MAILER_ERROR = "Failed to send email [%s]";
static final String GENERIC_ERROR = "Failed to send email [%s], reason: Third party error";
static final String INVALID_ENCODING = "Failed to send email [%s], reason: Encoding not accepted";
static final String UNKNOWN_ERROR = "Failed to send email [%s], reason: Unknown error";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ abstract class MailerGenericBuilderImpl<T extends MailerGenericBuilderImpl<?>> i
@Nullable
private Email emailOverrides;

/**
* @see MailerGenericBuilder#withMaximumEmailSize(int)
*/
@Nullable
private Integer maximumEmailSize;

/**
* @see MailerGenericBuilder#signByDefaultWithSmime(Pkcs12Config)
*/
Expand Down Expand Up @@ -296,7 +302,12 @@ private void validateProxy() {
* For internal use.
*/
EmailGovernance buildEmailGovernance() {
return new EmailGovernance(getEmailValidator(), getPkcs12ConfigForSmimeSigning(), getEmailDefaults(), getEmailOverrides());
return new EmailGovernance(
getEmailValidator(),
getPkcs12ConfigForSmimeSigning(),
getEmailDefaults(),
getEmailOverrides(),
getMaximumEmailSize());
}

/**
Expand Down Expand Up @@ -454,6 +465,15 @@ public T withEmailOverrides(@NotNull Email emailOverrides) {
return (T) this;
}

/**
* @see MailerGenericBuilder#withMaximumEmailSize(int)
*/
@Override
public T withMaximumEmailSize(int maximumEmailSize) {
this.maximumEmailSize = maximumEmailSize;
return (T) this;
}

/**
* @param pkcs12StoreFile The file containing the keystore
* @param storePassword The password to get keys from the store
Expand Down Expand Up @@ -843,6 +863,15 @@ public T clearEmailOverrides() {
return (T) this;
}

/**
* @see MailerGenericBuilder#clearMaximumEmailSize()
*/
@Override
public T clearMaximumEmailSize() {
this.maximumEmailSize = null;
return (T) this;
}

/**
* @see MailerGenericBuilder#clearSignByDefaultWithSmime()
*/
Expand Down Expand Up @@ -974,6 +1003,15 @@ public Email getEmailOverrides() {
return emailOverrides;
}

/**
* @see MailerGenericBuilder#getMaximumEmailSize()
*/
@Override
@Nullable
public Integer getMaximumEmailSize() {
return maximumEmailSize;
}

/**
* @see MailerGenericBuilder#getPkcs12ConfigForSmimeSigning()
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
import org.jetbrains.annotations.Nullable;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.internal.authenticatedsockssupport.socks5server.AnonymousSocks5Server;
import org.simplejavamail.api.mailer.EmailTooBigException;
import org.simplejavamail.api.mailer.config.OperationalConfig;
import org.simplejavamail.mailer.internal.util.TransportRunner;

import java.util.concurrent.atomic.AtomicInteger;

import static java.lang.String.format;
import static java.util.Optional.ofNullable;
import static org.simplejavamail.mailer.internal.MailerException.GENERIC_ERROR;
import static org.simplejavamail.mailer.internal.MailerException.MAILER_ERROR;
import static org.simplejavamail.mailer.internal.MailerException.UNKNOWN_ERROR;

/**
Expand Down Expand Up @@ -53,13 +56,18 @@ public void executeClosure() {
}
} catch (final MessagingException e) {
handleException(e, GENERIC_ERROR);
} catch (final MailerException | EmailTooBigException e) {
handleException(e, MAILER_ERROR);
} catch (final Exception e) {
handleException(e, UNKNOWN_ERROR);
}
}

private void handleException(final Exception e, String errorMsg) {
LOGGER.trace("Failed to send email {}\n{}", email.getId(), email);
throw new MailerException(format(errorMsg, email.getId()), e);
LOGGER.trace("Failed to send email {}\n{}\n\t{}", email.getId(), email, errorMsg);
val emailId = ofNullable(email.getId())
.map(id -> format("ID: '%s'", id))
.orElse(format("Subject: '%s'", email.getSubject()));
throw new MailerException(format(errorMsg, emailId), e);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.mailer.EmailTooBigException;
import org.simplejavamail.api.mailer.config.EmailGovernance;
import org.simplejavamail.api.mailer.config.OperationalConfig;
import org.simplejavamail.converter.internal.mimemessage.MimeMessageProducerHelper;
import org.simplejavamail.mailer.internal.util.SessionLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

import static java.lang.String.format;
Expand Down Expand Up @@ -54,12 +57,29 @@ public static void unprimeSession(@NotNull Session session) {
@NotNull
public static MimeMessage convertAndLogMimeMessage(Session session, final Email email) throws MessagingException {
val mimeMessageConverter = (SessionBasedEmailToMimeMessageConverter) session.getProperties().get(MIMEMESSAGE_CONVERTER_KEY);
return mimeMessageConverter.convertAndLogMimeMessage(email);
val mimeMessage = mimeMessageConverter.convertAndLogMimeMessage(email);
val governance = mimeMessageConverter.emailGovernance;

if (governance.getMaximumEmailSize() != null) {
val emailSize = calculateEmailSize(mimeMessage);
if (emailSize > governance.getMaximumEmailSize()) {
throw new EmailTooBigException(emailSize, governance.getMaximumEmailSize());
}
}
return mimeMessage;
}

private static int calculateEmailSize(MimeMessage mimeMessage) throws MessagingException {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
mimeMessage.writeTo(os);
return os.size();
} catch (IOException e) {
throw new RuntimeException("error trying to calculate email size", e);
}
}

@NotNull
private MimeMessage convertAndLogMimeMessage(final Email email) throws MessagingException {
// fill and send wrapped mime message parts
val message = convertMimeMessage(email, session, emailGovernance);

SessionLogger.logSession(session, operationalConfig.isAsync(), "mail");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.simplejavamail.mailer;

import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.internet.MimeMessage;
import lombok.val;
import org.jetbrains.annotations.NotNull;
Expand All @@ -14,7 +15,10 @@
import org.simplejavamail.api.email.OriginalSmimeDetails.SmimeMode;
import org.simplejavamail.api.email.Recipient;
import org.simplejavamail.api.internal.smimesupport.model.PlainSmimeDetails;
import org.simplejavamail.api.mailer.CustomMailer;
import org.simplejavamail.api.mailer.EmailTooBigException;
import org.simplejavamail.api.mailer.Mailer;
import org.simplejavamail.api.mailer.config.OperationalConfig;
import org.simplejavamail.converter.EmailConverter;
import org.simplejavamail.email.EmailBuilder;
import org.simplejavamail.email.internal.InternalEmailPopulatingBuilder;
Expand All @@ -41,6 +45,7 @@
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.data.MapEntry.entry;
import static org.simplejavamail.api.email.ContentTransferEncoding.BIT7;
import static org.simplejavamail.converter.EmailConverter.mimeMessageToEmail;
Expand Down Expand Up @@ -541,4 +546,59 @@ private void assertAttachmentMetadata(AttachmentResource embeddedImg, String mim
assertThat(embeddedImg.getDataSource().getContentType()).isEqualTo(mimeType);
assertThat(embeddedImg.getName()).isEqualTo(filename);
}

@Test
public void testMaximumEmailSize() {
val mailer = MailerBuilder
.withSMTPServer("localhost", SERVER_PORT, USERNAME, PASSWORD)
.withMaximumEmailSize(4)
.buildMailer();

sendAndVerifyEmailTooBigException(mailer);
}

@Test
public void testMaximumEmailSize_CustomMailer() {
val mailer = MailerBuilder
.withCustomMailer(new CustomMailer() {
@Override
public void testConnection(@NotNull OperationalConfig operationalConfig, @NotNull Session session) {
throw new RuntimeException("should reach here");
}

@Override
public void sendMessage(@NotNull OperationalConfig operationalConfig, @NotNull Session session, @NotNull Email email, @NotNull MimeMessage message) {
throw new RuntimeException("should reach here");
}
})
.withMaximumEmailSize(4)
.buildMailer();

sendAndVerifyEmailTooBigException(mailer);
}

@Test
public void testMaximumEmailSize_DontSendOnlyLog() {
val mailer = MailerBuilder
.withTransportModeLoggingOnly()
.withMaximumEmailSize(4)
.buildMailer();

sendAndVerifyEmailTooBigException(mailer);
}

private static void sendAndVerifyEmailTooBigException(Mailer mailer) {
val email = EmailBuilder.startingBlank()
.withPlainText("non empty text")
.withSubject("email size test")
.from("[email protected]")
.to("[email protected]")
.buildEmail();

assertThatThrownBy(() -> mailer.sendMail(email))
.hasMessageStartingWith("Failed to send email [ID:")
.getCause()
.isInstanceOf(EmailTooBigException.class)
.hasMessageContaining("bytes exceeds maximum allowed size of 4 bytes");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public void trustHosts() {

@Test
public void testSignWithSmime_WithConfigObject() {
final EmailGovernance emailGovernance = new EmailGovernance(null, loadPkcs12KeyStore(), null, null);
final EmailGovernance emailGovernance = new EmailGovernance(null, loadPkcs12KeyStore(), null, null, null);
final Mailer mailer = new MailerImpl(null, SMTP, emailGovernance, createEmptyProxyConfig(), session, createDummyOperationalConfig(EMPTY_LIST, true, false));

assertThat(mailer.getEmailGovernance().getPkcs12ConfigForSmimeSigning()).isNotNull();
Expand Down

0 comments on commit 8f39f04

Please sign in to comment.