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>
This commit is contained in:
Shawn O. Pearce
2009-09-10 18:13:33 -07:00
parent 3f4b1d08f6
commit 5c31bd7510
11 changed files with 668 additions and 18 deletions

View File

@@ -655,6 +655,43 @@ and all other properties of section sendemail are ignored.
+
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

View File

@@ -22,11 +22,23 @@ import java.util.Map;
/** 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);

View File

@@ -27,7 +27,8 @@ public class AccountState {
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;

View File

@@ -49,6 +49,8 @@ import com.google.gerrit.server.mail.AddReviewerSender;
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 @@ public class GerritGlobalModule extends FactoryModule {
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);

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -26,7 +26,6 @@ import com.google.gerrit.client.reviewdb.ReviewDb;
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.gwtorm.client.OrmException;
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;
@@ -92,6 +90,9 @@ public abstract class OutgoingEmail {
@Inject
private PatchListCache patchListCache;
@Inject
private FromAddressGenerator fromAddressGenerator;
@Inject
private EmailSender emailSender;
@@ -106,10 +107,6 @@ public abstract class OutgoingEmail {
@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 @@ public abstract class OutgoingEmail {
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 @@ public abstract class OutgoingEmail {
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 @@ public abstract class OutgoingEmail {
}
}
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
//

View File

@@ -32,6 +32,19 @@ public class ParamertizedStringTest extends TestCase {
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());

View File

@@ -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();
}
}

View File

@@ -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;
}
}