Use quoted-printable for SMTP transfer encoding

Some MTAs insert newlines into email content to limit line length to
around 1000 characters. Because the HTML part of Gerrit emails used HTML
breaks for line wrapping, lines in the raw message would easily exceed
1000 characters. At best, the newline would be inserted amid text,
resulting on odd prose or an incorrect code quote. At worst, the newline
would appear in HTML code, resulting in an invalid tree.

With this change, the text and HTML parts of emails sent through SMTP
are converted to the quoted-printable content-transfer-encoding. In this
way, lines are limited to 76 characters via "soft breaks".

With this encoding, the raw email content will only contain 7-bit ASCII
characters, which avoids other potential issues with older MTAs.

Bug: Issue 6527
Change-Id: I9adc5b4d69f4bd3f2bc580ff62de1e8b0f4ebbb6
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 7bb67aa..a8289c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -30,6 +30,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.Writer;
 import java.text.SimpleDateFormat;
@@ -45,6 +46,7 @@
 import org.apache.commons.net.smtp.AuthSMTPClient;
 import org.apache.commons.net.smtp.SMTPClient;
 import org.apache.commons.net.smtp.SMTPReply;
+import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
 import org.eclipse.jgit.lib.Config;
 
 /** Sends email via a nearby SMTP server. */
@@ -302,16 +304,26 @@
     throw new EmailException("Gave up generating unique MIME boundary");
   }
 
-  protected String buildMultipartBody(String boundary, String textPart, String htmlPart) {
+  protected String buildMultipartBody(String boundary, String textPart, String htmlPart)
+      throws IOException {
+    String encodedTextPart = quotedPrintableEncode(textPart);
+    String encodedHtmlPart = quotedPrintableEncode(htmlPart);
+
+    // Only declare quoted-printable encoding if there are characters that need to be encoded.
+    String textTransferEncoding = textPart.equals(encodedTextPart) ? "7bit" : "quoted-printable";
+    String htmlTransferEncoding = htmlPart.equals(encodedHtmlPart) ? "7bit" : "quoted-printable";
+
     return
     // Output the text part:
     "--"
         + boundary
         + "\r\n"
         + "Content-Type: text/plain; charset=UTF-8\r\n"
-        + "Content-Transfer-Encoding: 8bit\r\n"
+        + "Content-Transfer-Encoding: "
+        + textTransferEncoding
         + "\r\n"
-        + textPart
+        + "\r\n"
+        + encodedTextPart
         + "\r\n"
 
         // Output the HTML part:
@@ -319,9 +331,11 @@
         + boundary
         + "\r\n"
         + "Content-Type: text/html; charset=UTF-8\r\n"
-        + "Content-Transfer-Encoding: 8bit\r\n"
+        + "Content-Transfer-Encoding: "
+        + htmlTransferEncoding
         + "\r\n"
-        + htmlPart
+        + "\r\n"
+        + encodedHtmlPart
         + "\r\n"
 
         // Output the closing boundary.
@@ -330,6 +344,14 @@
         + "--\r\n";
   }
 
+  protected String quotedPrintableEncode(String input) throws IOException {
+    ByteArrayOutputStream s = new ByteArrayOutputStream();
+    try (QuotedPrintableOutputStream qp = new QuotedPrintableOutputStream(s, false)) {
+      qp.write(input.getBytes(UTF_8));
+    }
+    return s.toString();
+  }
+
   private static void setMissingHeader(Map<String, EmailHeader> hdrs, String name, String value) {
     if (!hdrs.containsKey(name) || hdrs.get(name).isEmpty()) {
       hdrs.put(name, new EmailHeader.String(value));