Merge "Modify EmailSender interface to optionally accept HTML bodies"
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;
}