Modify EmailSender interface to optionally accept HTML bodies
Because some EmailSender implementations need to handle the HTML part of
the message separately, the sender interface is modified to admit an
additional body parameter. This new, expanded version of the send method
signature is given a default method body so that existing
implementations will continue to work (albeit plaintext-only).
The implementation of SmtpEmailSender is modified to take advantage of
the new signature.
Bug: Issue 4589
Change-Id: I748970aa1812a61394d6991bb4060191755eb8ad
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
index a7a1028..d36225a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.errors.EmailException;
import java.util.Collection;
@@ -32,7 +33,36 @@
boolean canEmail(String address);
/**
- * Sends an email message.
+ * Sends an email message. Messages always contain a text body, but messages
+ * can optionally include an additional HTML body. If both body types are
+ * present, {@code send} should construct a {@code multipart/alternative}
+ * message with an appropriately-selected boundary.
+ *
+ * @param from who the message is from.
+ * @param rcpt one or more address where the message will be delivered to.
+ * This list overrides any To or CC headers in {@code headers}.
+ * @param headers message headers.
+ * @param textBody text to appear in the {@code text/plain} body of the
+ * message.
+ * @param htmlBody optional HTML code to appear in the {@code text/html} body
+ * of the message.
+ * @throws EmailException the message cannot be sent.
+ */
+ default void send(Address from, Collection<Address> rcpt,
+ Map<String, EmailHeader> headers, String textBody,
+ @Nullable String htmlBody) throws EmailException {
+ send(from, rcpt, headers, textBody);
+ }
+
+ /**
+ * Sends an email message with a text body only (i.e. not HTML or multipart).
+ *
+ * Authors of new implementations of this interface should not use this method
+ * to send a message because this method does not accept the HTML body.
+ * Instead, authors should use the above signature of {@code send}.
+ *
+ * This version of the method is preserved for support of legacy
+ * implementations.
*
* @param from who the message is from.
* @param rcpt one or more address where the message will be delivered to.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index c3b69e3..1565567 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -18,7 +18,6 @@
import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -55,7 +54,6 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.ThreadLocalRandom;
/** Sends an email to one or more interested parties. */
public abstract class OutgoingEmail {
@@ -157,20 +155,11 @@
va.smtpRcptTo = smtpRcptTo;
va.headers = headers;
+ va.body = textPart;
if (useHtml()) {
- String htmlPart = htmlBody.toString();
- String boundary = generateMultipartBoundary(textPart, htmlPart);
-
- va.body = buildMultipartBody(boundary, textPart, htmlPart);
- va.textBody = textPart;
- va.htmlBody = htmlPart;
- va.headers.put("Content-Type", new EmailHeader.String(
- "multipart/alternative; "
- + "boundary=\"" + boundary + "\"; "
- + "charset=UTF-8"));
+ va.htmlBody = htmlBody.toString();
} else {
- va.body = textPart;
- va.textBody = textPart;
+ va.htmlBody = null;
}
for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
@@ -181,53 +170,11 @@
}
}
- args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body);
+ args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers,
+ va.body, va.htmlBody);
}
}
- protected String buildMultipartBody(String boundary, String textPart,
- String htmlPart) {
- return
- // Output the text part:
- "--" + boundary + "\r\n"
- + "Content-Type: text/plain; charset=UTF-8\r\n"
- + "Content-Transfer-Encoding: 8bit\r\n"
- + "\r\n"
- + textPart + "\r\n"
-
- // Output the HTML part:
- + "--" + boundary + "\r\n"
- + "Content-Type: text/html; charset=UTF-8\r\n"
- + "Content-Transfer-Encoding: 8bit\r\n"
- + "\r\n"
- + htmlPart + "\r\n"
-
- // Output the closing boundary.
- + "--" + boundary + "--\r\n";
- }
-
- protected String generateMultipartBoundary(String textBody, String htmlBody)
- throws EmailException {
- byte[] bytes = new byte[8];
- ThreadLocalRandom rng = ThreadLocalRandom.current();
-
- // The probability of the boundary being valid is approximately
- // (2^64 - len(message)) / 2^64.
- //
- // The message is much shorter than 2^64 bytes, so if two tries don't
- // suffice, something is seriously wrong.
- for (int i = 0; i < 2; i++) {
- rng.nextBytes(bytes);
- String boundary = BaseEncoding.base64().encode(bytes);
- String encBoundary = "--" + boundary;
- if (textBody.contains(encBoundary) || htmlBody.contains(encBoundary)) {
- continue;
- }
- return boundary;
- }
- throw new EmailException("Gave up generating unique MIME boundary");
- }
-
/** Format the message body by calling {@link #appendText(String)}. */
protected abstract void format() throws EmailException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index e263c6a..3b4fcfc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -16,7 +16,9 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.Version;
import com.google.gerrit.common.errors.EmailException;
@@ -42,6 +44,7 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/** Sends email via a nearby SMTP server. */
@@ -146,8 +149,15 @@
@Override
public void send(final Address from, Collection<Address> rcpt,
- final Map<String, EmailHeader> callerHeaders, final String body)
+ final Map<String, EmailHeader> callerHeaders, String body)
throws EmailException {
+ send(from, rcpt, callerHeaders, body, null);
+ }
+
+ @Override
+ public void send(final Address from, Collection<Address> rcpt,
+ final Map<String, EmailHeader> callerHeaders, String textBody,
+ @Nullable String htmlBody) throws EmailException {
if (!isEnabled()) {
throw new EmailException("Sending email is disabled");
}
@@ -155,7 +165,6 @@
final Map<String, EmailHeader> hdrs =
new LinkedHashMap<>(callerHeaders);
setMissingHeader(hdrs, "MIME-Version", "1.0");
- setMissingHeader(hdrs, "Content-Type", "text/plain; charset=UTF-8");
setMissingHeader(hdrs, "Content-Transfer-Encoding", "8bit");
setMissingHeader(hdrs, "Content-Disposition", "inline");
setMissingHeader(hdrs, "User-Agent", "Gerrit/" + Version.getVersion());
@@ -169,6 +178,18 @@
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(expiry));
}
+ String encodedBody;
+ if (htmlBody == null) {
+ setMissingHeader(hdrs, "Content-Type", "text/plain; charset=UTF-8");
+ encodedBody = textBody;
+ } else {
+ String boundary = generateMultipartBoundary(textBody, htmlBody);
+ setMissingHeader(hdrs, "Content-Type", "multipart/alternative; "
+ + "boundary=\"" + boundary + "\"; "
+ + "charset=UTF-8");
+ encodedBody = buildMultipartBody(boundary, textBody, htmlBody);
+ }
+
StringBuffer rejected = new StringBuffer();
try {
final SMTPClient client = open();
@@ -214,7 +235,7 @@
}
w.write("\r\n");
- w.write(body);
+ w.write(encodedBody);
w.flush();
}
@@ -235,6 +256,49 @@
}
}
+ public static String generateMultipartBoundary(String textBody,
+ String htmlBody) throws EmailException {
+ byte[] bytes = new byte[8];
+ ThreadLocalRandom rng = ThreadLocalRandom.current();
+
+ // The probability of the boundary being valid is approximately
+ // (2^64 - len(message)) / 2^64.
+ //
+ // The message is much shorter than 2^64 bytes, so if two tries don't
+ // suffice, something is seriously wrong.
+ for (int i = 0; i < 2; i++) {
+ rng.nextBytes(bytes);
+ String boundary = BaseEncoding.base64().encode(bytes);
+ String encBoundary = "--" + boundary;
+ if (textBody.contains(encBoundary) || htmlBody.contains(encBoundary)) {
+ continue;
+ }
+ return boundary;
+ }
+ throw new EmailException("Gave up generating unique MIME boundary");
+ }
+
+ protected String buildMultipartBody(String boundary, String textPart,
+ String htmlPart) {
+ return
+ // Output the text part:
+ "--" + boundary + "\r\n"
+ + "Content-Type: text/plain; charset=UTF-8\r\n"
+ + "Content-Transfer-Encoding: 8bit\r\n"
+ + "\r\n"
+ + textPart + "\r\n"
+
+ // Output the HTML part:
+ + "--" + boundary + "\r\n"
+ + "Content-Type: text/html; charset=UTF-8\r\n"
+ + "Content-Transfer-Encoding: 8bit\r\n"
+ + "\r\n"
+ + htmlPart + "\r\n"
+
+ // Output the closing boundary.
+ + "--" + boundary + "--\r\n";
+ }
+
private void setMissingHeader(final Map<String, EmailHeader> hdrs,
final String name, final String value) {
if (!hdrs.containsKey(name) || hdrs.get(name).isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
index bfec979..2182ac1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
@@ -33,13 +33,12 @@
class Args {
// in arguments
public String messageClass;
- public String textBody;
@Nullable public String htmlBody;
// in/out arguments
public Address smtpFromAddress;
public Set<Address> smtpRcptTo;
- public String body;
+ public String body; // The text body of the email.
public Map<String, EmailHeader> headers;
}