Use quoted printable strings in outgoing email

If we are sending non-ASCII strings in the headers of an email
message we should protect them using quoted printable encoding
with a UTF-8 character set.

Bug: issue 387
Change-Id: I19038f7568124e2ca5900222c77110e688158437
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index cc207e7..1640e05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import java.io.UnsupportedEncodingException;
+
 class Address {
   static Address parse(final String in) {
     final int lt = in.indexOf('<');
@@ -46,10 +48,14 @@
 
   @Override
   public String toString() {
-    return toHeaderString();
+    try {
+      return toHeaderString();
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException("Cannot encode address", e);
+    }
   }
 
-  String toHeaderString() {
+  String toHeaderString() throws UnsupportedEncodingException {
     if (name != null) {
       return quotedPhrase(name) + " <" + email + ">";
     } else if (isSimple()) {
@@ -71,10 +77,14 @@
     return true;
   }
 
-  private static String quotedPhrase(final String name) {
+  private static String quotedPhrase(final String name)
+      throws UnsupportedEncodingException {
+    if (EmailHeader.needsQuotedPrintable(name)) {
+      return EmailHeader.quotedPrintable(name);
+    }
     for (int i = 0; i < name.length(); i++) {
       final char c = name.charAt(i);
-      if (c < ' ' || 0x7F <= c || MUST_QUOTE_NAME.indexOf(c) != -1) {
+      if (MUST_QUOTE_NAME.indexOf(c) != -1) {
         return wrapInQuotes(name);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
index 36a5f37..bab1118 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -40,10 +41,48 @@
 
     @Override
     void write(Writer w) throws IOException {
-      w.write(value);
+      if (needsQuotedPrintable(value)) {
+        w.write(quotedPrintable(value));
+      } else {
+        w.write(value);
+      }
     }
   }
 
+  static boolean needsQuotedPrintable(java.lang.String value) {
+    for (int i = 0; i < value.length(); i++) {
+      if (value.charAt(i) < ' ' || '~' < value.charAt(i)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  static java.lang.String quotedPrintable(java.lang.String value)
+      throws UnsupportedEncodingException {
+    final StringBuilder r = new StringBuilder();
+    final byte[] encoded = value.getBytes("UTF-8");
+
+    r.append("=?UTF-8?Q?");
+    for (int i = 0; i < encoded.length; i++) {
+      byte b = encoded[i];
+      if (b == ' ') {
+        r.append('_');
+
+      } else if (b == '=' || b == '"' || b == '_' || b < ' ' || '~' <= b) {
+        r.append('=');
+        r.append(Integer.toHexString((b >>> 4) & 0x0f).toUpperCase());
+        r.append(Integer.toHexString(b & 0x0f).toUpperCase());
+
+      } else {
+        r.append((char) b);
+      }
+    }
+    r.append("?=");
+
+    return r.toString();
+  }
+
   static class Date extends EmailHeader {
     private java.util.Date value;
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
index bfa5f4f..fbbd72b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
@@ -16,6 +16,8 @@
 
 import junit.framework.TestCase;
 
+import java.io.UnsupportedEncodingException;
+
 public class AddressTest extends TestCase {
   public void testParse_NameEmail1() {
     final Address a = Address.parse("A U Thor <author@example.com>");
@@ -106,6 +108,10 @@
     assertEquals("\"A \\\" C\" <a@a>", format("A \" C", "a@a"));
   }
 
+  public void testToHeaderString_NameEmail6() {
+    assertEquals("=?UTF-8?Q?A_=E2=82=AC_B?= <a@a>", format("A \u20ac B", "a@a"));
+  }
+
   public void testToHeaderString_Email1() {
     assertEquals("a@a", format(null, "a@a"));
   }
@@ -115,6 +121,10 @@
   }
 
   private static String format(final String name, final String email) {
-    return new Address(name, email).toHeaderString();
+    try {
+      return new Address(name, email).toHeaderString();
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException("Cannot encode address", e);
+    }
   }
 }