Add sendemail.from to control setting From header

Configuring sendemail.from enables the site administrator to control
how Gerrit will setup the outing email's From header in the message
envelope.  Some sites may prefer using a single address that Gerrit
sends from, while others may prefer forging the user's own email if
they are all on the same domain.

Change-Id: Ie19c4a3c50cc92f39af6fe6b43ba8aabb37b5077
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6b35fc0..afa9526 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -655,6 +655,43 @@
 +
 By default, true, allowing notifications to be sent.
 
+[[sendemail.from]]sendemail.from::
++
+Designates what name and address Gerrit will place in the From
+field of any generated email messages.  The supported values are:
++
+* `USER`
++
+Gerrit will set the From header to use the current user's
+Full Name and Preferred Email.  This may cause messsages to be
+classified as spam if the user's domain has SPF or DKIM enabled
+and <<sendemail.smtpServer,sendemail.smtpServer>> is not a trusted
+relay for that domain.
++
+* `MIXED`
++
+Shorthand for `$\{user\} (Code Review) <review@example.com>` where
+`review@example.com` is the same as <<user.email,user.email>>.
+See below for a description of how the replacement is handled.
++
+* `SERVER`
++
+Gerrit will set the From header to the same name and address
+it records in any commits Gerrit creates.  This is set by
+<<user.name,user.name>> and <<user.email,user.email>>, or guessed
+from the local operating system.
++
+* 'Code Review' `<`'review'`@`'example.com'`>`
++
+If set to a name and email address in brackets, Gerrit will use
+this name and email address for any messages, overriding the name
+that may have been selected for commits by user.name and user.email.
+Optionally, the name portion may contain the placeholder `$\{user\}`,
+which is replaced by the Full Name of the current user.
+
++
+By default, MIXED.
+
 [[sendemail.smtpServer]]sendemail.smtpServer::
 +
 Hostname (or IP address) of a SMTP server that will relay
diff --git a/src/main/java/com/google/gerrit/server/ParamertizedString.java b/src/main/java/com/google/gerrit/server/ParamertizedString.java
index 719a5d4..0e39644 100644
--- a/src/main/java/com/google/gerrit/server/ParamertizedString.java
+++ b/src/main/java/com/google/gerrit/server/ParamertizedString.java
@@ -22,11 +22,23 @@
 
 /** Performs replacements on strings such as <code>Hello ${user}</code>. */
 public class ParamertizedString {
+  /** Obtain a string which has no parameters and always produces the value. */
+  public static ParamertizedString asis(final String constant) {
+    return new ParamertizedString(new Constant(constant));
+  }
+
   private final String pattern;
   private final String rawPattern;
   private final List<Format> patternOps;
   private final List<String> patternArgs;
 
+  private ParamertizedString(final Constant c) {
+    pattern = c.text;
+    rawPattern = c.text;
+    patternOps = Collections.<Format> singletonList(c);
+    patternArgs = Collections.emptyList();
+  }
+
   public ParamertizedString(final String pattern) {
     final StringBuilder raw = new StringBuilder();
     final List<String> args = new ArrayList<String>(4);
diff --git a/src/main/java/com/google/gerrit/server/account/AccountState.java b/src/main/java/com/google/gerrit/server/account/AccountState.java
index efa624d..2210cff 100644
--- a/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -27,7 +27,8 @@
   private final Set<AccountGroup.Id> internalGroups;
   private final Collection<AccountExternalId> externalIds;
 
-  AccountState(final Account account, final Set<AccountGroup.Id> actualGroups,
+  public AccountState(final Account account,
+      final Set<AccountGroup.Id> actualGroups,
       final Collection<AccountExternalId> externalIds) {
     this.account = account;
     this.internalGroups = actualGroups;
diff --git a/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 24f7a7a..99c7dfd 100644
--- a/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -49,6 +49,8 @@
 import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.EmailSender;
+import com.google.gerrit.server.mail.FromAddressGenerator;
+import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
@@ -147,7 +149,10 @@
     factory(MergeOp.Factory.class);
     factory(ReloadSubmitQueueOp.Factory.class);
 
+    bind(FromAddressGenerator.class).toProvider(
+        FromAddressGeneratorProvider.class).in(SINGLETON);
     bind(EmailSender.class).to(SmtpEmailSender.class).in(SINGLETON);
+
     factory(PatchSetImporter.Factory.class);
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
diff --git a/src/main/java/com/google/gerrit/server/mail/Address.java b/src/main/java/com/google/gerrit/server/mail/Address.java
index 9668188..cc207e7 100644
--- a/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -15,8 +15,25 @@
 package com.google.gerrit.server.mail;
 
 class Address {
-  String name;
-  String email;
+  static Address parse(final String in) {
+    final int lt = in.indexOf('<');
+    final int gt = in.indexOf('>');
+    final int at = in.indexOf("@");
+    if (0 <= lt && lt < gt && lt + 1 < at && at + 1 < gt) {
+      final String email = in.substring(lt + 1, gt).trim();
+      final String name = in.substring(0, lt).trim();
+      return new Address(name.length() > 0 ? name : null, email);
+    }
+
+    if (lt < 0 && gt < 0 && 0 < at && at < in.length() - 1) {
+      return new Address(in);
+    }
+
+    throw new IllegalArgumentException("Invalid email address: " + in);
+  }
+
+  final String name;
+  final String email;
 
   Address(String email) {
     this(null, email);
diff --git a/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java b/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java
new file mode 100644
index 0000000..512e5e8
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.client.reviewdb.Account;
+
+/** Constructs an address to send email from. */
+public interface FromAddressGenerator {
+  public Address from(Account.Id fromId);
+}
diff --git a/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
new file mode 100644
index 0000000..7ebc845
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.client.reviewdb.Account;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.ParamertizedString;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.spearce.jgit.lib.Config;
+import org.spearce.jgit.lib.PersonIdent;
+
+/** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */
+@Singleton
+public class FromAddressGeneratorProvider implements
+    Provider<FromAddressGenerator> {
+  private final FromAddressGenerator generator;
+
+  @Inject
+  FromAddressGeneratorProvider(@GerritServerConfig final Config cfg,
+      @GerritPersonIdent final PersonIdent myIdent,
+      final AccountCache accountCache) {
+
+    final String from = cfg.getString("sendemail", null, "from");
+    final Address srvAddr = toAddress(myIdent);
+
+    if (from == null || "MIXED".equalsIgnoreCase(from)) {
+      final String name = "${user} (Code Review)";
+      final String email = srvAddr.email;
+      generator = new PatternGen(srvAddr, accountCache, name, email);
+
+    } else if ("USER".equalsIgnoreCase(from)) {
+      generator = new UserGen(accountCache, srvAddr);
+
+    } else if ("SERVER".equalsIgnoreCase(from)) {
+      generator = new ServerGen(srvAddr);
+
+    } else {
+      final Address a = Address.parse(from);
+      generator = new PatternGen(srvAddr, accountCache, a.name, a.email);
+    }
+  }
+
+  private static Address toAddress(final PersonIdent myIdent) {
+    return new Address(myIdent.getName(), myIdent.getEmailAddress());
+  }
+
+  @Override
+  public FromAddressGenerator get() {
+    return generator;
+  }
+
+  static final class UserGen implements FromAddressGenerator {
+    private final AccountCache accountCache;
+    private final Address srvAddr;
+
+    UserGen(AccountCache accountCache, Address srvAddr) {
+      this.accountCache = accountCache;
+      this.srvAddr = srvAddr;
+    }
+
+    @Override
+    public Address from(final Account.Id fromId) {
+      if (fromId != null) {
+        final Account a = accountCache.get(fromId).getAccount();
+        if (a.getPreferredEmail() != null) {
+          return new Address(a.getFullName(), a.getPreferredEmail());
+        }
+      }
+      return srvAddr;
+    }
+  }
+
+  static final class ServerGen implements FromAddressGenerator {
+    private final Address srvAddr;
+
+    ServerGen(Address srvAddr) {
+      this.srvAddr = srvAddr;
+    }
+
+    @Override
+    public Address from(final Account.Id fromId) {
+      return srvAddr;
+    }
+  }
+
+  static final class PatternGen implements FromAddressGenerator {
+    private final String senderEmail;
+    private final Address serverAddress;
+    private final AccountCache accountCache;
+    private final ParamertizedString namePattern;
+
+    PatternGen(final Address serverAddress, final AccountCache accountCache,
+        final String namePattern, final String senderEmail) {
+      this.senderEmail = senderEmail;
+      this.serverAddress = serverAddress;
+      this.accountCache = accountCache;
+      this.namePattern = new ParamertizedString(namePattern);
+    }
+
+    @Override
+    public Address from(final Account.Id fromId) {
+      final String senderName;
+
+      if (fromId != null) {
+        final Account account = accountCache.get(fromId).getAccount();
+        String fullName = account.getFullName();
+        if (fullName == null || "".equals(fullName)) {
+          fullName = "Anonymous Coward";
+        }
+        senderName = namePattern.replace("user", fullName).toString();
+
+      } else {
+        senderName = serverAddress.name;
+      }
+
+      return new Address(senderName, senderEmail);
+    }
+  }
+}
diff --git a/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index c404d3e..f902ed9 100644
--- a/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.client.reviewdb.StarredChange;
 import com.google.gerrit.client.reviewdb.UserIdentity;
 import com.google.gerrit.git.GitRepositoryManager;
-import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -42,7 +41,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.spearce.jgit.lib.PersonIdent;
 import org.spearce.jgit.util.SystemReader;
 
 import java.net.MalformedURLException;
@@ -93,6 +91,9 @@
   private PatchListCache patchListCache;
 
   @Inject
+  private FromAddressGenerator fromAddressGenerator;
+
+  @Inject
   private EmailSender emailSender;
 
   @Inject
@@ -106,10 +107,6 @@
   @Nullable
   private Provider<String> urlProvider;
 
-  @Inject
-  @GerritPersonIdent
-  private PersonIdent gerritIdent;
-
   private ProjectState projectState;
 
   protected OutgoingEmail(final Change c, final String mc) {
@@ -219,7 +216,7 @@
       projectName = null;
     }
 
-    smtpFromAddress = computeFrom();
+    smtpFromAddress = fromAddressGenerator.from(fromId);
     if (changeMessage != null && changeMessage.getWrittenOn() != null) {
       setHeader("Date", new Date(changeMessage.getWrittenOn().getTime()));
     } else {
@@ -232,6 +229,19 @@
       setChangeSubjectHeader();
     }
     setHeader("Message-ID", "");
+
+    if (fromId != null) {
+      // If we have a user that this message is supposedly caused by
+      // but the From header on the email does not match the user as
+      // it is a generic header for this Gerrit server, include the
+      // Reply-To header with the current user's email address.
+      //
+      final Address a = toAddress(fromId);
+      if (a != null && !smtpFromAddress.email.equals(a.email)) {
+        setHeader("Reply-To", a.email);
+      }
+    }
+
     setHeader("X-Gerrit-MessageType", messageClass);
     if (change != null) {
       setHeader("X-Gerrit-Change-Id", "" + change.getKey().get());
@@ -261,14 +271,6 @@
     }
   }
 
-  private Address computeFrom() {
-    if (fromId != null) {
-      return toAddress(fromId);
-    }
-
-    return new Address(gerritIdent.getName(), gerritIdent.getEmailAddress());
-  }
-
   private void setListIdHeader() {
     // Set a reasonable list id so that filters can be used to sort messages
     //
diff --git a/src/test/java/com/google/gerrit/server/ParamertizedStringTest.java b/src/test/java/com/google/gerrit/server/ParamertizedStringTest.java
index de74c9d..82dd1c1 100644
--- a/src/test/java/com/google/gerrit/server/ParamertizedStringTest.java
+++ b/src/test/java/com/google/gerrit/server/ParamertizedStringTest.java
@@ -32,6 +32,19 @@
     assertEquals("", p.replace(a));
   }
 
+  public void testAsis1() {
+    final ParamertizedString p = ParamertizedString.asis("${bar}c");
+    assertEquals("${bar}c", p.getPattern());
+    assertEquals("${bar}c", p.getRawPattern());
+    assertTrue(p.getParameterNames().isEmpty());
+
+    final Map<String, String> a = new HashMap<String, String>();
+    a.put("bar", "frobinator");
+    assertNotNull(p.bind(a));
+    assertEquals(0, p.bind(a).length);
+    assertEquals("${bar}c", p.replace(a));
+  }
+
   public void testReplace1() {
     final ParamertizedString p = new ParamertizedString("${bar}c");
     assertEquals("${bar}c", p.getPattern());
diff --git a/src/test/java/com/google/gerrit/server/mail/AddressTest.java b/src/test/java/com/google/gerrit/server/mail/AddressTest.java
new file mode 100644
index 0000000..bfa5f4f
--- /dev/null
+++ b/src/test/java/com/google/gerrit/server/mail/AddressTest.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import junit.framework.TestCase;
+
+public class AddressTest extends TestCase {
+  public void testParse_NameEmail1() {
+    final Address a = Address.parse("A U Thor <author@example.com>");
+    assertEquals("A U Thor", a.name);
+    assertEquals("author@example.com", a.email);
+  }
+
+  public void testParse_NameEmail2() {
+    final Address a = Address.parse("A <a@b>");
+    assertEquals("A", a.name);
+    assertEquals("a@b", a.email);
+  }
+
+  public void testParse_NameEmail3() {
+    final Address a = Address.parse("<a@b>");
+    assertNull(a.name);
+    assertEquals("a@b", a.email);
+  }
+
+  public void testParse_NameEmail4() {
+    final Address a = Address.parse("A U Thor<author@example.com>");
+    assertEquals("A U Thor", a.name);
+    assertEquals("author@example.com", a.email);
+  }
+
+  public void testParse_NameEmail5() {
+    final Address a = Address.parse("A U Thor  <author@example.com>");
+    assertEquals("A U Thor", a.name);
+    assertEquals("author@example.com", a.email);
+  }
+
+  public void testParse_Email1() {
+    final Address a = Address.parse("author@example.com");
+    assertNull(a.name);
+    assertEquals("author@example.com", a.email);
+  }
+
+  public void testParse_Email2() {
+    final Address a = Address.parse("a@b");
+    assertNull(a.name);
+    assertEquals("a@b", a.email);
+  }
+
+  public void testParseInvalid() {
+    assertInvalid("");
+    assertInvalid("a");
+    assertInvalid("a<");
+    assertInvalid("<a");
+    assertInvalid("<a>");
+    assertInvalid("a<a>");
+    assertInvalid("a <a>");
+
+    assertInvalid("a");
+    assertInvalid("a<@");
+    assertInvalid("<a@");
+    assertInvalid("<a@>");
+    assertInvalid("a<a@>");
+    assertInvalid("a <a@>");
+    assertInvalid("a <@a>");
+  }
+
+  private static void assertInvalid(final String in) {
+    try {
+      Address.parse(in);
+      fail("Incorrectly accepted " + in);
+    } catch (IllegalArgumentException e) {
+      assertEquals("Invalid email address: " + in, e.getMessage());
+    }
+  }
+
+  public void testToHeaderString_NameEmail1() {
+    assertEquals("A <a@a>", format("A", "a@a"));
+  }
+
+  public void testToHeaderString_NameEmail2() {
+    assertEquals("A B <a@a>", format("A B", "a@a"));
+  }
+
+  public void testToHeaderString_NameEmail3() {
+    assertEquals("\"A B. C\" <a@a>", format("A B. C", "a@a"));
+  }
+
+  public void testToHeaderString_NameEmail4() {
+    assertEquals("\"A B, C\" <a@a>", format("A B, C", "a@a"));
+  }
+
+  public void testToHeaderString_NameEmail5() {
+    assertEquals("\"A \\\" C\" <a@a>", format("A \" C", "a@a"));
+  }
+
+  public void testToHeaderString_Email1() {
+    assertEquals("a@a", format(null, "a@a"));
+  }
+
+  public void testToHeaderString_Email2() {
+    assertEquals("<a,b@a>", format(null, "a,b@a"));
+  }
+
+  private static String format(final String name, final String email) {
+    return new Address(name, email).toHeaderString();
+  }
+}
diff --git a/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
new file mode 100644
index 0000000..78bea43
--- /dev/null
+++ b/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -0,0 +1,285 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import com.google.gerrit.client.reviewdb.Account;
+import com.google.gerrit.client.reviewdb.AccountExternalId;
+import com.google.gerrit.client.reviewdb.AccountGroup;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+
+import junit.framework.TestCase;
+
+import org.spearce.jgit.lib.Config;
+import org.spearce.jgit.lib.PersonIdent;
+
+import java.util.Collections;
+
+public class FromAddressGeneratorProviderTest extends TestCase {
+  private Config config;
+  private PersonIdent ident;
+  private AccountCache accountCache;
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    config = new Config();
+    ident = new PersonIdent("NAME", "e@email", 0, 0);
+    accountCache = createStrictMock(AccountCache.class);
+  }
+
+  private FromAddressGenerator create() {
+    return new FromAddressGeneratorProvider(config, ident, accountCache).get();
+  }
+
+  private void setFrom(final String newFrom) {
+    config.setString("sendemail", null, "from", newFrom);
+  }
+
+  public void testDefaultIsMIXED() {
+    assertTrue(create() instanceof FromAddressGeneratorProvider.PatternGen);
+  }
+
+  public void testSelectUSER() {
+    setFrom("USER");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.UserGen);
+
+    setFrom("user");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.UserGen);
+
+    setFrom("uSeR");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.UserGen);
+  }
+
+  public void testUSER_FullyConfiguredUser() {
+    setFrom("USER");
+
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals(name, r.name);
+    assertEquals(email, r.email);
+    verify(accountCache);
+  }
+
+  public void testUSER_NoFullNameUser() {
+    setFrom("USER");
+
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(null, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals(null, r.name);
+    assertEquals(email, r.email);
+    verify(accountCache);
+  }
+
+  public void testUSER_NoPreferredEmailUser() {
+    setFrom("USER");
+
+    final Account.Id user = user("A U. Thor", null);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals(ident.getName(), r.name);
+    assertEquals(ident.getEmailAddress(), r.email);
+    verify(accountCache);
+  }
+
+  public void testUSER_NullUser() {
+    setFrom("USER");
+    replay(accountCache);
+    final Address r = create().from(null);
+    assertNotNull(r);
+    assertEquals(ident.getName(), r.name);
+    assertEquals(ident.getEmailAddress(), r.email);
+    verify(accountCache);
+  }
+
+  public void testSelectSERVER() {
+    setFrom("SERVER");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.ServerGen);
+
+    setFrom("server");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.ServerGen);
+
+    setFrom("sErVeR");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.ServerGen);
+  }
+
+  public void testSERVER_FullyConfiguredUser() {
+    setFrom("SERVER");
+
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = userNoLookup(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals(ident.getName(), r.name);
+    assertEquals(ident.getEmailAddress(), r.email);
+    verify(accountCache);
+  }
+
+  public void testSERVER_NullUser() {
+    setFrom("SERVER");
+    replay(accountCache);
+    final Address r = create().from(null);
+    assertNotNull(r);
+    assertEquals(ident.getName(), r.name);
+    assertEquals(ident.getEmailAddress(), r.email);
+    verify(accountCache);
+  }
+
+  public void testSelectMIXED() {
+    setFrom("MIXED");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.PatternGen);
+
+    setFrom("mixed");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.PatternGen);
+
+    setFrom("mIxEd");
+    assertTrue(create() instanceof FromAddressGeneratorProvider.PatternGen);
+  }
+
+  public void testMIXED_FullyConfiguredUser() {
+    setFrom("MIXED");
+
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals(name + " (Code Review)", r.name);
+    assertEquals(ident.getEmailAddress(), r.email);
+    verify(accountCache);
+  }
+
+  public void testMIXED_NoFullNameUser() {
+    setFrom("MIXED");
+
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(null, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals("Anonymous Coward (Code Review)", r.name);
+    assertEquals(ident.getEmailAddress(), r.email);
+    verify(accountCache);
+  }
+
+  public void testMIXED_NoPreferredEmailUser() {
+    setFrom("MIXED");
+
+    final String name = "A U. Thor";
+    final Account.Id user = user(name, null);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals(name + " (Code Review)", r.name);
+    assertEquals(ident.getEmailAddress(), r.email);
+    verify(accountCache);
+  }
+
+  public void testMIXED_NullUser() {
+    setFrom("MIXED");
+    replay(accountCache);
+    final Address r = create().from(null);
+    assertNotNull(r);
+    assertEquals(ident.getName(), r.name);
+    assertEquals(ident.getEmailAddress(), r.email);
+    verify(accountCache);
+  }
+
+  public void testCUSTOM_FullyConfiguredUser() {
+    setFrom("A ${user} B <my.server@email.address>");
+
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals("A " + name + " B", r.name);
+    assertEquals("my.server@email.address", r.email);
+    verify(accountCache);
+  }
+
+  public void testCUSTOM_NoFullNameUser() {
+    setFrom("A ${user} B <my.server@email.address>");
+
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(null, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertNotNull(r);
+    assertEquals("A Anonymous Coward B", r.name);
+    assertEquals("my.server@email.address", r.email);
+    verify(accountCache);
+  }
+
+  public void testCUSTOM_NullUser() {
+    setFrom("A ${user} B <my.server@email.address>");
+
+    replay(accountCache);
+    final Address r = create().from(null);
+    assertNotNull(r);
+    assertEquals(ident.getName(), r.name);
+    assertEquals("my.server@email.address", r.email);
+    verify(accountCache);
+  }
+
+  private Account.Id user(final String name, final String email) {
+    final AccountState s = makeUser(name, email);
+    expect(accountCache.get(eq(s.getAccount().getId()))).andReturn(s);
+    return s.getAccount().getId();
+  }
+
+  private Account.Id userNoLookup(final String name, final String email) {
+    final AccountState s = makeUser(name, email);
+    return s.getAccount().getId();
+  }
+
+  private AccountState makeUser(final String name, final String email) {
+    final Account.Id userId = new Account.Id(42);
+    final Account account = new Account(userId);
+    account.setFullName(name);
+    account.setPreferredEmail(email);
+    final AccountState s =
+        new AccountState(account, Collections.<AccountGroup.Id> emptySet(),
+            Collections.<AccountExternalId> emptySet());
+    return s;
+  }
+}