@@ -30,55 +30,114 @@ public class MailerHelper {
3030
3131 private static final Logger LOGGER = getLogger (MailerHelper .class );
3232
33+ /**
34+ * Delegates to all other validations for a full checkup.
35+ *
36+ * @see #validateCompleteness(Email)
37+ * @see #validateAddresses(Email, EmailValidator)
38+ * @see #scanForInjectionAttacks(Email)
39+ */
3340 @ SuppressWarnings ({ "SameReturnValue" })
3441 public static boolean validate (@ NotNull final Email email , @ Nullable final EmailValidator emailValidator )
3542 throws MailException {
3643 LOGGER .debug ("validating email..." );
3744
45+ validateCompleteness (email );
46+ validateAddresses (email , emailValidator );
47+ scanForInjectionAttacks (email );
48+
49+ LOGGER .debug ("...no problems found" );
50+
51+ return true ;
52+ }
53+
54+ /**
55+ * Checks whether:
56+ * <ol>
57+ * <li>there are recipients</li>
58+ * <li>if there is a sender</li>
59+ * <li>if there is a disposition notification TO if flag is set to use it</li>
60+ * <li>if there is a return receipt TO if flag is set to use it</li>
61+ * </ol>
62+ */
63+ public static void validateCompleteness (final @ NotNull Email email ) {
3864 // check for mandatory values
3965 if (email .getRecipients ().size () == 0 ) {
40- throw new MailValidationException ( MailValidationException .MISSING_RECIPIENT );
66+ throw new MailCompletenessException ( MailCompletenessException .MISSING_RECIPIENT );
4167 } else if (email .getFromRecipient () == null ) {
42- throw new MailValidationException ( MailValidationException .MISSING_SENDER );
68+ throw new MailCompletenessException ( MailCompletenessException .MISSING_SENDER );
4369 } else if (email .isUseDispositionNotificationTo () && email .getDispositionNotificationTo () == null ) {
44- throw new MailValidationException ( MailValidationException .MISSING_DISPOSITIONNOTIFICATIONTO );
70+ throw new MailCompletenessException ( MailCompletenessException .MISSING_DISPOSITIONNOTIFICATIONTO );
4571 } else if (email .isUseReturnReceiptTo () && email .getReturnReceiptTo () == null ) {
46- throw new MailValidationException (MailValidationException .MISSING_RETURNRECEIPTTO );
47- } else
72+ throw new MailCompletenessException (MailCompletenessException .MISSING_RETURNRECEIPTTO );
73+ }
74+ }
75+
76+ /**
77+ * If email validator is provided, checks:
78+ * <ol>
79+ * <li>from recipient</li>
80+ * <li>all TO/CC/BCC recipients</li>
81+ * <li>reply-to recipient, if provided</li>
82+ * <li>bounce-to recipient, if provided</li>
83+ * <li>disposition-notification-to recipient, if provided</li>
84+ * <li>return-receipt-to recipient, if provided</li>
85+ * </ol>
86+ */
87+ public static void validateAddresses (final @ NotNull Email email , final @ Nullable EmailValidator emailValidator ) {
4888 if (emailValidator != null ) {
4989 if (!emailValidator .isValid (email .getFromRecipient ().getAddress ())) {
50- throw new MailValidationException (format (MailValidationException .INVALID_SENDER , email ));
90+ throw new MailInvalidAddressException (format (MailInvalidAddressException .INVALID_SENDER , email ));
5191 }
5292 for (final Recipient recipient : email .getRecipients ()) {
5393 if (!emailValidator .isValid (recipient .getAddress ())) {
54- throw new MailValidationException (format (MailValidationException .INVALID_RECIPIENT , email ));
94+ throw new MailInvalidAddressException (format (MailInvalidAddressException .INVALID_RECIPIENT , email ));
5595 }
5696 }
5797 if (email .getReplyToRecipient () != null && !emailValidator .isValid (email .getReplyToRecipient ().getAddress ())) {
58- throw new MailValidationException (format (MailValidationException .INVALID_REPLYTO , email ));
98+ throw new MailInvalidAddressException (format (MailInvalidAddressException .INVALID_REPLYTO , email ));
5999 }
60100 if (email .getBounceToRecipient () != null && !emailValidator .isValid (email .getBounceToRecipient ().getAddress ())) {
61- throw new MailValidationException (format (MailValidationException .INVALID_BOUNCETO , email ));
101+ throw new MailInvalidAddressException (format (MailInvalidAddressException .INVALID_BOUNCETO , email ));
62102 }
63103 if (email .isUseDispositionNotificationTo ()) {
64104 if (!emailValidator .isValid (checkNonEmptyArgument (email .getDispositionNotificationTo (), "dispositionNotificationTo" ).getAddress ())) {
65- throw new MailValidationException (format (MailValidationException .INVALID_DISPOSITIONNOTIFICATIONTO , email ));
105+ throw new MailInvalidAddressException (format (MailInvalidAddressException .INVALID_DISPOSITIONNOTIFICATIONTO , email ));
66106 }
67107 }
68108 if (email .isUseReturnReceiptTo ()) {
69109 if (!emailValidator .isValid (checkNonEmptyArgument (email .getReturnReceiptTo (), "returnReceiptTo" ).getAddress ())) {
70- throw new MailValidationException (format (MailValidationException .INVALID_RETURNRECEIPTTO , email ));
110+ throw new MailInvalidAddressException (format (MailInvalidAddressException .INVALID_RETURNRECEIPTTO , email ));
71111 }
72112 }
73113 }
114+ }
74115
116+ /**
117+ * Checks the following headers for suspicious content (newlines and characters):
118+ * <ol>
119+ * <li>subject</li>
120+ * <li>every header name and value</li>
121+ * <li>every attachment name, nested datasource name and description</li>
122+ * <li>every embedded image name, nested datasource name and description</li>
123+ * <li>from recipient name and address</li>
124+ * <li>replyTo recipient name and address, if provided</li>
125+ * <li>bounceTo recipient name and address, if provided</li>
126+ * <li>every TO/CC/BCC recipient name and address</li>
127+ * <li>disposition-notification-to recipient name and address, if provided</li>
128+ * <li>return-receipt-to recipient name and address, if provided</li>
129+ * </ol>
130+ *
131+ * @see #scanForInjectionAttack
132+ */
133+ public static void scanForInjectionAttacks (final @ NotNull Email email ) {
75134 // check for illegal values
76135 scanForInjectionAttack (email .getSubject (), "email.subject" );
77136 for (final Map .Entry <String , Collection <String >> headerEntry : email .getHeaders ().entrySet ()) {
78137 for (final String headerValue : headerEntry .getValue ()) {
79138 // FIXME is this still needed?
80139 scanForInjectionAttack (headerEntry .getKey (), "email.header.headerName" );
81- scanForInjectionAttack (MimeUtility .unfold (headerValue ), "email.header." + headerEntry .getKey ());
140+ scanForInjectionAttack (MimeUtility .unfold (headerValue ), format ( "email.header.[%s]" , headerEntry .getKey () ));
82141 }
83142 }
84143 for (final AttachmentResource attachment : email .getAttachments ()) {
@@ -113,24 +172,20 @@ public static boolean validate(@NotNull final Email email, @Nullable final Email
113172 scanForInjectionAttack (recipient .getName (), "email.recipient.name" );
114173 scanForInjectionAttack (recipient .getAddress (), "email.recipient.address" );
115174 }
116-
117- LOGGER .debug ("...no problems found" );
118-
119- return true ;
120175 }
121176
122177 /**
123- * @param value Value checked for suspicious newline characters "\n", "\r" and "%0A" (as acknowledged by SMTP servers).
178+ * @param value Value checked for suspicious newline characters "\n", "\r" and the URL-encoded newline "%0A" (as acknowledged by SMTP servers).
124179 * @param valueLabel The name of the field being checked, used for reporting exceptions.
125180 *
126181 * @see <a href="https://web.archive.org/web/20160331233647/http://www.cakesolutions.net/teamblogs/2008/05/08/email-header-injection-security">Email Header Injection security</a>
127182 * @see <a href="https://security.stackexchange.com/a/54100/110048">StackExchange - What threats come from CRLF in email generation?</a>
128183 * @see <a href="https://archive.ph/NuETu">OWASP - Testing for IMAP SMTP Injection</a>
129184 * @see <a href="https://archive.ph/uReuD">CWE-93: Improper Neutralization of CRLF Sequences ('CRLF Injection')</a>
130185 */
131- private static void scanForInjectionAttack (final @ Nullable String value , final String valueLabel ) {
186+ public static void scanForInjectionAttack (final @ Nullable String value , final String valueLabel ) {
132187 if (value != null && (value .contains ("\n " ) || value .contains ("\r " ) || value .contains ("%0A" ))) {
133- throw new MailValidationException (format (MailValidationException .INJECTION_SUSPECTED , valueLabel , value ));
188+ throw new MailSuspiciousCRLFValueException (format (MailSuspiciousCRLFValueException .INJECTION_SUSPECTED , valueLabel , value ));
134189 }
135190 }
136191
@@ -152,4 +207,4 @@ public static MimeMessage signAndOrEncryptMessageWithSmime(@NotNull final Sessio
152207 return ModuleLoader .loadSmimeModule ()
153208 .signAndOrEncryptEmail (session , messageToProtect , emailContainingSmimeDetails , defaultSmimeSigningStore );
154209 }
155- }
210+ }
0 commit comments