All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.simplejavamail.mailer.MailerHelper Maven / Gradle / Ivy

There is a newer version: 8.12.4
Show newest version
/*
 * Copyright © 2009 Benny Bottema ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.simplejavamail.mailer;

import com.sanctionco.jmail.EmailValidator;
import jakarta.mail.Session;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeUtility;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.simplejavamail.MailException;
import org.simplejavamail.api.email.AttachmentResource;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.email.Recipient;
import org.simplejavamail.api.email.config.DkimConfig;
import org.simplejavamail.api.email.config.SmimeEncryptionConfig;
import org.simplejavamail.api.email.config.SmimeSigningConfig;
import org.simplejavamail.internal.moduleloader.ModuleLoader;
import org.slf4j.Logger;

import java.util.Collection;
import java.util.Map;

import static jakarta.mail.Message.RecipientType.TO;
import static java.lang.Boolean.TRUE;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * If you don't need to actually connect to a server and/or send anything, then this class still provides you with some functionality
 * otherwise triggered only by a {@link org.simplejavamail.api.mailer.Mailer}.
 */
public class MailerHelper {

	private static final Logger LOGGER = getLogger(MailerHelper.class);

	/**
	 * Delegates to #validate(Email, EmailValidator) with a null validator.
	 */
	@SuppressWarnings({ "SameReturnValue" })
	public static boolean validate(@NotNull final Email email)
			throws MailException {
		return validate(email, null);
	}

	/**
	 * Delegates to all other validations for a full checkup.
	 *
	 * @see #validateCompleteness(Email)
	 * @see #validateAddresses(Email, EmailValidator)
	 * @see #scanForInjectionAttacks(Email)
	 */
	@SuppressWarnings({ "SameReturnValue" })
	public static boolean validate(@NotNull final Email email, @Nullable final EmailValidator emailValidator)
			throws MailException {
		LOGGER.debug("validating email...");

		validateCompleteness(email);
		validateAddresses(email, emailValidator);
		scanForInjectionAttacks(email);

		LOGGER.debug("...no problems found");

		return true;
	}

	/**
	 * Delegate to #validateLenient(Email, EmailValidator) with a null validator.
	 */
	public static boolean validateLenient(@NotNull final Email email)
			throws MailException {
		return validateLenient(email, null);
	}

	/**
	 * Lenient validation only checks for missing fields (which implies incorrect configuration or missing data),
	 * but only warns for invalid address and suspected CRLF injections.
	 *
	 * @see #validateCompleteness(Email)
	 * @see #validateAddresses(Email, EmailValidator)
	 * @see #scanForInjectionAttacks(Email) 
	 */
	@SuppressWarnings({ "SameReturnValue" })
	public static boolean validateLenient(@NotNull final Email email, @Nullable final EmailValidator emailValidator)
			throws MailException {
		LOGGER.debug("validating email...");
		try {
			MailerHelper.validateCompleteness(email);
		} catch (MailCompletenessException e) {
			LOGGER.warn("encountered (and ignored) missing field: {}", e.getMessage());
		}
		try {
			MailerHelper.validateAddresses(email, emailValidator);
		} catch (MailInvalidAddressException e) {
			LOGGER.warn("encountered (and ignored) invalid address: {}", e.getMessage());
		}
		try {
			MailerHelper.scanForInjectionAttacks(email);
		} catch (MailSuspiciousCRLFValueException e) {
			LOGGER.warn("encountered (and ignored) suspected CRLF injection: {}", e.getMessage());
		}
		LOGGER.debug("...no blocking problems found");

		return true;
	}

	/**
	 * Checks whether the following RFC 5322 mandatory properties are present:
	 * 
    *
  1. there are recipients
  2. *
  3. if there is a sender
  4. *
*/ public static void validateCompleteness(final @NotNull Email email) { // check for mandatory values if (email.getRecipients().isEmpty()) { throw new MailCompletenessException(MailCompletenessException.MISSING_RECIPIENT); } else if (email.getFromRecipient() == null) { throw new MailCompletenessException(MailCompletenessException.MISSING_SENDER); } } /** * If email validator is provided, checks: *
    *
  1. from recipient
  2. *
  3. all TO/CC/BCC recipients
  4. *
  5. reply-to recipient, if provided
  6. *
  7. bounce-to recipient, if provided
  8. *
  9. disposition-notification-to recipient, if provided
  10. *
  11. return-receipt-to recipient, if provided
  12. *
*/ public static void validateAddresses(final @NotNull Email email, final @Nullable EmailValidator emailValidator) { if (emailValidator != null) { validateAddress(emailValidator, email.getFromRecipient(), MailInvalidAddressException.INVALID_SENDER); for (final Recipient recipient : email.getRecipients()) { switch (ofNullable(recipient.getType()).orElse(TO).toString()) { case "Cc": validateAddress(emailValidator, recipient, MailInvalidAddressException.INVALID_CC_RECIPIENT); break; case "Bcc": validateAddress(emailValidator, recipient, MailInvalidAddressException.INVALID_BCC_RECIPIENT); break; case "To": default: validateAddress(emailValidator, recipient, MailInvalidAddressException.INVALID_TO_RECIPIENT); break; } } for (final Recipient recipient : email.getReplyToRecipients()) { validateAddress(emailValidator, recipient, MailInvalidAddressException.INVALID_REPLYTO); } validateAddress(emailValidator, email.getBounceToRecipient(), MailInvalidAddressException.INVALID_BOUNCETO); if (TRUE.equals(email.getUseDispositionNotificationTo()) && email.getDispositionNotificationTo() != null) { validateAddress(emailValidator, email.getDispositionNotificationTo(), MailInvalidAddressException.INVALID_DISPOSITIONNOTIFICATIONTO); } if (TRUE.equals(email.getUseReturnReceiptTo()) && email.getReturnReceiptTo() != null) { validateAddress(emailValidator, email.getReturnReceiptTo(), MailInvalidAddressException.INVALID_RETURNRECEIPTTO); } } } private static void validateAddress(@NotNull EmailValidator emailValidator, @Nullable Recipient recipient, @NotNull String errorTemplate) { if (recipient != null && !emailValidator.isValid(recipient.getAddress())) { throw new MailInvalidAddressException(format(errorTemplate, recipient.getAddress())); } } /** * Checks the following headers for suspicious content (newlines and characters): *
    *
  1. subject
  2. *
  3. every header name and value
  4. *
  5. every attachment name, nested datasource name and description
  6. *
  7. every embedded image name, nested datasource name and description
  8. *
  9. from recipient name and address
  10. *
  11. replyTo recipient name and address, if provided
  12. *
  13. bounceTo recipient name and address, if provided
  14. *
  15. every TO/CC/BCC recipient name and address
  16. *
  17. disposition-notification-to recipient name and address, if provided
  18. *
  19. return-receipt-to recipient name and address, if provided
  20. *
* * @see #scanForInjectionAttack */ public static void scanForInjectionAttacks(final @NotNull Email email) { // check for illegal values scanForInjectionAttack(email.getSubject(), "email.subject"); for (final Map.Entry> headerEntry : email.getHeaders().entrySet()) { for (final String headerValue : headerEntry.getValue()) { // TODO is this still needed? scanForInjectionAttack(headerEntry.getKey(), "email.header.headerName"); scanForInjectionAttack(MimeUtility.unfold(headerValue), format("email.header.[%s]", headerEntry.getKey())); } } for (final AttachmentResource attachment : email.getAttachments()) { scanForInjectionAttack(attachment.getName(), "email.attachment.name"); scanForInjectionAttack(attachment.getDataSource().getName(), "email.attachment.datasource.name"); scanForInjectionAttack(attachment.getDescription(), "email.attachment.description"); } for (final AttachmentResource embeddedImage : email.getEmbeddedImages()) { scanForInjectionAttack(embeddedImage.getName(), "email.embeddedImage.name"); scanForInjectionAttack(embeddedImage.getDataSource().getName(), "email.embeddedImage.datasource.name"); scanForInjectionAttack(embeddedImage.getDescription(), "email.embeddedImage.description"); } if (!valueNullOrEmpty(email.getFromRecipient())) { scanForInjectionAttack(email.getFromRecipient().getName(), "email.fromRecipient.name"); scanForInjectionAttack(email.getFromRecipient().getAddress(), "email.fromRecipient.address"); } for (final Recipient recipient : email.getReplyToRecipients()) { scanForInjectionAttack(recipient.getName(), "email.replyToRecipient.name"); scanForInjectionAttack(recipient.getAddress(), "email.replyToRecipient.address"); } if (!valueNullOrEmpty(email.getBounceToRecipient())) { scanForInjectionAttack(email.getBounceToRecipient().getName(), "email.bounceToRecipient.name"); scanForInjectionAttack(email.getBounceToRecipient().getAddress(), "email.bounceToRecipient.address"); } if (!valueNullOrEmpty(email.getDispositionNotificationTo())) { scanForInjectionAttack(email.getDispositionNotificationTo().getName(), "email.dispositionNotificationTo.name"); scanForInjectionAttack(email.getDispositionNotificationTo().getAddress(), "email.dispositionNotificationTo.address"); } if (!valueNullOrEmpty(email.getReturnReceiptTo())) { scanForInjectionAttack(email.getReturnReceiptTo().getName(), "email.returnReceiptTo.name"); scanForInjectionAttack(email.getReturnReceiptTo().getAddress(), "email.returnReceiptTo.address"); } for (final Recipient recipient : email.getRecipients()) { scanForInjectionAttack(recipient.getName(), "email.recipient.name"); scanForInjectionAttack(recipient.getAddress(), "email.recipient.address"); } } /** * @param value Value checked for suspicious newline characters "\n", "\r" and the URL-encoded newline "%0A" (as acknowledged by SMTP servers). * @param valueLabel The name of the field being checked, used for reporting exceptions. * * @see Email Header Injection security * @see StackExchange - What threats come from CRLF in email generation? * @see OWASP - Testing for IMAP SMTP Injection * @see CWE-93: Improper Neutralization of CRLF Sequences ('CRLF Injection') */ public static void scanForInjectionAttack(final @Nullable String value, final String valueLabel) { if (value != null && (value.contains("\n") || value.contains("\r") || value.contains("%0A"))) { final String s = value.replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r"); throw new MailSuspiciousCRLFValueException(format(MailSuspiciousCRLFValueException.INJECTION_SUSPECTED, valueLabel, s)); } } /** * @see org.simplejavamail.internal.modules.DKIMModule#signMessageWithDKIM(Email, MimeMessage, DkimConfig, Recipient) */ @SuppressWarnings("unused") public static MimeMessage signMessageWithDKIM(@NotNull final MimeMessage messageToSign, @NotNull final Email emailContainingSigningDetails) { val dkimConfig = requireNonNull(emailContainingSigningDetails.getDkimConfig(), "email.dkimConfig"); val fromRecipient = requireNonNull(emailContainingSigningDetails.getFromRecipient(), "email.fromRecipient"); return ModuleLoader.loadDKIMModule().signMessageWithDKIM(emailContainingSigningDetails, messageToSign, dkimConfig, fromRecipient); } /** * Depending on the Email configuration, signs and then encrypts message (both steps optional), using the S/MIME module. * * @see org.simplejavamail.internal.modules.SMIMEModule#signMessageWithSmime(Session, Email, MimeMessage, SmimeSigningConfig) * @see org.simplejavamail.internal.modules.SMIMEModule#encryptMessageWithSmime(Session, Email, MimeMessage, SmimeEncryptionConfig) */ @SuppressWarnings("unused") public static MimeMessage signAndOrEncryptMessageWithSmime(@NotNull final Session session, @NotNull final MimeMessage messageToProtect, @NotNull final Email emailContainingSmimeDetails) { MimeMessage message = messageToProtect; message = ModuleLoader.loadSmimeModule().signMessageWithSmime(session, emailContainingSmimeDetails, message, requireNonNull(emailContainingSmimeDetails.getSmimeSigningConfig(), "SmimeSigningConfig")); message = ModuleLoader.loadSmimeModule().encryptMessageWithSmime(session, emailContainingSmimeDetails, message, requireNonNull(emailContainingSmimeDetails.getSmimeEncryptionConfig(), "SmimeEncryptionConfig")); return message; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy