Support project notification using To or CC

Some teams want to have new change notifications be CC'd
to a mailing list so that reply-all goes to the list.
Add notify.*.header = cc support to allow this usage.

Change-Id: I037b823a4127fe4d2ba0248e6be7f7efd7544b1c
This commit is contained in:
Shawn O. Pearce
2012-10-25 17:02:50 -07:00
parent 6738e5f340
commit aedcb7e808
10 changed files with 88 additions and 33 deletions

View File

@@ -99,6 +99,16 @@ are sent.
+ +
Like email, this variable may be a list of options. Like email, this variable may be a list of options.
[[notify.name.header]]notify.<name>.header::
+
Email header used to list the destination. If not set BCC is used.
Only one value may be specified. To use different headers for each
address list them in different notify blocks.
+
* `to`: The standard To field is used; addresses are visible to all.
* `cc`: The standard CC field is used; addresses are visible to all.
* `bcc`: SMTP RCPT TO is used to hide the address.
[[notify.name.filter]]notify.<name>.filter:: [[notify.name.filter]]notify.<name>.filter::
+ +
link:user-search.html[Change search expression] to match changes that link:user-search.html[Change search expression] to match changes that

View File

@@ -24,10 +24,15 @@ import java.util.EnumSet;
import java.util.Set; import java.util.Set;
public class NotifyConfig implements Comparable<NotifyConfig> { public class NotifyConfig implements Comparable<NotifyConfig> {
public static enum Header {
TO, CC, BCC;
}
private String name; private String name;
private EnumSet<NotifyType> types = EnumSet.of(NotifyType.ALL); private EnumSet<NotifyType> types = EnumSet.of(NotifyType.ALL);
private String filter; private String filter;
private Header header;
private Set<GroupReference> groups = Sets.newHashSet(); private Set<GroupReference> groups = Sets.newHashSet();
private Set<Address> addresses = Sets.newHashSet(); private Set<Address> addresses = Sets.newHashSet();
@@ -63,6 +68,14 @@ public class NotifyConfig implements Comparable<NotifyConfig> {
} }
} }
public Header getHeader() {
return header;
}
public void setHeader(Header hdr) {
header = hdr;
}
public Set<GroupReference> getGroups() { public Set<GroupReference> getGroups() {
return groups; return groups;
} }

View File

@@ -79,6 +79,7 @@ public class ProjectConfig extends VersionedMetaData {
private static final String KEY_EMAIL = "email"; private static final String KEY_EMAIL = "email";
private static final String KEY_FILTER = "filter"; private static final String KEY_FILTER = "filter";
private static final String KEY_TYPE = "type"; private static final String KEY_TYPE = "type";
private static final String KEY_HEADER = "header";
private static final String CAPABILITY = "capability"; private static final String CAPABILITY = "capability";
@@ -368,6 +369,9 @@ public class ProjectConfig extends VersionedMetaData {
NOTIFY, sectionName, KEY_TYPE, NOTIFY, sectionName, KEY_TYPE,
NotifyType.ALL)); NotifyType.ALL));
n.setTypes(types); n.setTypes(types);
n.setHeader(ConfigUtil.getEnum(rc,
NOTIFY, sectionName, KEY_HEADER,
NotifyConfig.Header.BCC));
for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) { for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
if (dst.startsWith("group ")) { if (dst.startsWith("group ")) {
@@ -593,6 +597,8 @@ public class ProjectConfig extends VersionedMetaData {
Collections.sort(addrs); Collections.sort(addrs);
email.addAll(addrs); email.addAll(addrs);
set(rc, NOTIFY, nc.getName(), KEY_HEADER,
nc.getHeader(), NotifyConfig.Header.BCC);
if (email.isEmpty()) { if (email.isEmpty()) {
rc.unset(NOTIFY, nc.getName(), KEY_EMAIL); rc.unset(NOTIFY, nc.getName(), KEY_EMAIL);
} else { } else {

View File

@@ -39,7 +39,7 @@ public class AbandonedSender extends ReplyToChangeSender {
ccAllApprovals(); ccAllApprovals();
bccStarredBy(); bccStarredBy();
bccWatches(NotifyType.ALL_COMMENTS); includeWatchers(NotifyType.ALL_COMMENTS);
} }
@Override @Override

View File

@@ -322,16 +322,13 @@ public abstract class ChangeEmail extends OutgoingEmail {
} }
} }
/** BCC users and groups that want notification of events. */ /** Include users and groups that want notification of events. */
protected void bccWatches(NotifyType type) { protected void includeWatchers(NotifyType type) {
try { try {
Watchers matching = getWatches(type); Watchers matching = getWatches(type);
for (Account.Id user : matching.accounts) { add(RecipientType.TO, matching.to);
add(RecipientType.BCC, user); add(RecipientType.CC, matching.cc);
} add(RecipientType.BCC, matching.bcc);
for (Address addr : matching.emails) {
add(RecipientType.BCC, addr);
}
} catch (OrmException err) { } catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those // Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people // we already have queued up then to fail deliver entirely to people
@@ -340,6 +337,16 @@ public abstract class ChangeEmail extends OutgoingEmail {
} }
} }
/** Add users or email addresses to the TO, CC, or BCC list. */
protected void add(RecipientType type, Watchers.List list) {
for (Account.Id user : list.accounts) {
add(type, user);
}
for (Address addr : list.emails) {
add(type, addr);
}
}
/** Returns all watches that are relevant */ /** Returns all watches that are relevant */
protected final Watchers getWatches(NotifyType type) throws OrmException { protected final Watchers getWatches(NotifyType type) throws OrmException {
Watchers matching = new Watchers(); Watchers matching = new Watchers();
@@ -385,9 +392,26 @@ public abstract class ChangeEmail extends OutgoingEmail {
} }
protected static class Watchers { protected static class Watchers {
static class List {
protected final Set<Account.Id> accounts = Sets.newHashSet(); protected final Set<Account.Id> accounts = Sets.newHashSet();
protected final Set<Address> emails = Sets.newHashSet(); protected final Set<Address> emails = Sets.newHashSet();
} }
protected final List to = new List();
protected final List cc = new List();
protected final List bcc = new List();
List list(NotifyConfig.Header header) {
switch (header) {
case TO:
return to;
case CC:
return cc;
default:
case BCC:
return bcc;
}
}
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void add(Watchers matching, NotifyConfig nc, Project.NameKey project) private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
@@ -419,7 +443,7 @@ public abstract class ChangeEmail extends OutgoingEmail {
p = args.queryRewriter.get().rewrite(p); p = args.queryRewriter.get().rewrite(p);
} }
if (p.match(changeData)) { if (p.match(changeData)) {
recursivelyAddAllAccounts(matching, group); recursivelyAddAllAccounts(matching.list(nc.getHeader()), group);
} }
} }
@@ -430,16 +454,16 @@ public abstract class ChangeEmail extends OutgoingEmail {
Predicate<ChangeData> p = qb.parse(nc.getFilter()); Predicate<ChangeData> p = qb.parse(nc.getFilter());
p = args.queryRewriter.get().rewrite(p); p = args.queryRewriter.get().rewrite(p);
if (p.match(changeData)) { if (p.match(changeData)) {
matching.emails.addAll(nc.getAddresses()); matching.list(nc.getHeader()).emails.addAll(nc.getAddresses());
} }
} else { } else {
matching.emails.addAll(nc.getAddresses()); matching.list(nc.getHeader()).emails.addAll(nc.getAddresses());
} }
} }
} }
private void recursivelyAddAllAccounts(Watchers matching, AccountGroup group) private void recursivelyAddAllAccounts(Watchers.List matching,
throws OrmException { AccountGroup group) throws OrmException {
Set<AccountGroup.Id> seen = Sets.newHashSet(); Set<AccountGroup.Id> seen = Sets.newHashSet();
Queue<AccountGroup.Id> scan = Lists.newLinkedList(); Queue<AccountGroup.Id> scan = Lists.newLinkedList();
scan.add(group.getId()); scan.add(group.getId());
@@ -472,13 +496,13 @@ public abstract class ChangeEmail extends OutgoingEmail {
p = Predicate.and(qb.parse(w.getFilter()), p); p = Predicate.and(qb.parse(w.getFilter()), p);
p = args.queryRewriter.get().rewrite(p); p = args.queryRewriter.get().rewrite(p);
if (p.match(changeData)) { if (p.match(changeData)) {
matching.accounts.add(w.getAccountId()); matching.bcc.accounts.add(w.getAccountId());
} }
} catch (QueryParseException e) { } catch (QueryParseException e) {
// Ignore broken filter expressions. // Ignore broken filter expressions.
} }
} else if (p.match(changeData)) { } else if (p.match(changeData)) {
matching.accounts.add(w.getAccountId()); matching.bcc.accounts.add(w.getAccountId());
} }
} }

View File

@@ -69,7 +69,7 @@ public class CommentSender extends ReplyToChangeSender {
ccAllApprovals(); ccAllApprovals();
bccStarredBy(); bccStarredBy();
bccWatches(NotifyType.ALL_COMMENTS); includeWatchers(NotifyType.ALL_COMMENTS);
} }
@Override @Override

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail; package com.google.gerrit.server.mail;
import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
@@ -47,25 +48,26 @@ public class CreateChangeSender extends NewChangeSender {
super.init(); super.init();
try { try {
// BCC anyone who has interest in this project's changes // Try to mark interested owners with TO and CC or BCC line.
// Try to mark interested owners with a TO and not a BCC line.
//
Watchers matching = getWatches(NotifyType.NEW_CHANGES); Watchers matching = getWatches(NotifyType.NEW_CHANGES);
for (Account.Id user : matching.accounts) { for (Account.Id user : Iterables.concat(
matching.to.accounts,
matching.cc.accounts,
matching.bcc.accounts)) {
if (isOwnerOfProjectOrBranch(user)) { if (isOwnerOfProjectOrBranch(user)) {
add(RecipientType.TO, user); add(RecipientType.TO, user);
} else {
add(RecipientType.BCC, user);
} }
} }
for (Address addr : matching.emails) {
add(RecipientType.BCC, addr); // Add everyone else. Owners added above will not be duplicated.
} add(RecipientType.TO, matching.to);
add(RecipientType.CC, matching.cc);
add(RecipientType.BCC, matching.bcc);
} catch (OrmException err) { } catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those // Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people // we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change. // who have a lower interest in the change.
log.warn("Cannot BCC watchers for new change", err); log.warn("Cannot notify watchers for new change", err);
} }
} }

View File

@@ -52,8 +52,8 @@ public class MergedSender extends ReplyToChangeSender {
ccAllApprovals(); ccAllApprovals();
bccStarredBy(); bccStarredBy();
bccWatches(NotifyType.ALL_COMMENTS); includeWatchers(NotifyType.ALL_COMMENTS);
bccWatches(NotifyType.SUBMITTED_CHANGES); includeWatchers(NotifyType.SUBMITTED_CHANGES);
} }
@Override @Override

View File

@@ -39,7 +39,7 @@ public class RestoredSender extends ReplyToChangeSender {
ccAllApprovals(); ccAllApprovals();
bccStarredBy(); bccStarredBy();
bccWatches(NotifyType.ALL_COMMENTS); includeWatchers(NotifyType.ALL_COMMENTS);
} }
@Override @Override

View File

@@ -38,7 +38,7 @@ public class RevertedSender extends ReplyToChangeSender {
ccAllApprovals(); ccAllApprovals();
bccStarredBy(); bccStarredBy();
bccWatches(NotifyType.ALL_COMMENTS); includeWatchers(NotifyType.ALL_COMMENTS);
} }
@Override @Override