Merge changes I9adc5b4d,Ide2b9025 into stable-2.14 * changes: Use quoted-printable for SMTP transfer encoding Refactor email rendering of SMTP sender
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 80e6bb8..48aff2e 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. */ @@ -169,33 +171,6 @@ throw new EmailException("Sending email is disabled"); } - final Map<String, EmailHeader> hdrs = new LinkedHashMap<>(callerHeaders); - setMissingHeader(hdrs, "MIME-Version", "1.0"); - setMissingHeader(hdrs, "Content-Transfer-Encoding", "8bit"); - setMissingHeader(hdrs, "Content-Disposition", "inline"); - setMissingHeader(hdrs, "User-Agent", "Gerrit/" + Version.getVersion()); - if (importance != null) { - setMissingHeader(hdrs, "Importance", importance); - } - if (expiryDays > 0) { - Date expiry = new Date(TimeUtil.nowMs() + expiryDays * 24 * 60 * 60 * 1000L); - setMissingHeader( - hdrs, "Expiry-Date", 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(); @@ -238,20 +213,8 @@ + " rejected DATA command: " + client.getReplyString()); } - try (Writer w = new BufferedWriter(messageDataWriter)) { - for (Map.Entry<String, EmailHeader> h : hdrs.entrySet()) { - if (!h.getValue().isEmpty()) { - w.write(h.getKey()); - w.write(": "); - h.getValue().write(w); - w.write("\r\n"); - } - } - w.write("\r\n"); - w.write(encodedBody); - w.flush(); - } + render(messageDataWriter, callerHeaders, textBody, htmlBody); if (!client.completePendingCommand()) { throw new EmailException( @@ -270,6 +233,55 @@ } } + private void render( + Writer out, + Map<String, EmailHeader> callerHeaders, + String textBody, + @Nullable String htmlBody) + throws IOException, EmailException { + final Map<String, EmailHeader> hdrs = new LinkedHashMap<>(callerHeaders); + setMissingHeader(hdrs, "MIME-Version", "1.0"); + setMissingHeader(hdrs, "Content-Transfer-Encoding", "8bit"); + setMissingHeader(hdrs, "Content-Disposition", "inline"); + setMissingHeader(hdrs, "User-Agent", "Gerrit/" + Version.getVersion()); + if (importance != null) { + setMissingHeader(hdrs, "Importance", importance); + } + if (expiryDays > 0) { + Date expiry = new Date(TimeUtil.nowMs() + expiryDays * 24 * 60 * 60 * 1000L); + setMissingHeader( + hdrs, "Expiry-Date", 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); + } + + try (Writer w = new BufferedWriter(out)) { + for (Map.Entry<String, EmailHeader> h : hdrs.entrySet()) { + if (!h.getValue().isEmpty()) { + w.write(h.getKey()); + w.write(": "); + h.getValue().write(w); + w.write("\r\n"); + } + } + + w.write("\r\n"); + w.write(encodedBody); + w.flush(); + } + } + public static String generateMultipartBoundary(String textBody, String htmlBody) throws EmailException { byte[] bytes = new byte[8]; @@ -292,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: @@ -309,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. @@ -320,8 +344,15 @@ + "--\r\n"; } - private void setMissingHeader( - final Map<String, EmailHeader> hdrs, final String name, final String value) { + 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)); }