Add sendemail.denyrcpt functionality

Currently sendemail.allowrcpt exists in the configuration
to whitelist specific emails or domains.

This add sendemail.denyrcpt to blacklist specific emails
or domains.
Example usecase:
To be able to block e-mails from being sent to service users that
don't have an email and preventing log-spam such as:
* EmailException: Recipient address rejected: User unknown in local
recipient table

denyrcpt is evaluated first.

Neither allowrcpt or denyrcpt will prevent account
creation but both will prevent confirmation emails.

Change-Id: I97d6b378dd0678689f65c42e79de68162c889a92
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 167f1cb..2c1d146 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -4392,6 +4392,19 @@
 If set to a domain name, any address at that domain can receive
 email from Gerrit.
 +
+If allowrcpt is configured, The set of allowed recipients is:
+`allowrcpt - denyrcpt`.
++
+By default, unset, permitting delivery to any email address.
+
+[[sendemail.denyrcpt]]sendemail.denyrcpt::
++
+If present, each value adds one entry to the blacklist of email
+addresses that Gerrit can send email to.  If set to a complete
+email address, that one address is added to the blacklist.
+If set to a domain name, any address at that domain can *not* receive
+email from Gerrit.
++
 By default, unset, permitting delivery to any email address.
 
 [[sendemail.includeDiff]]sendemail.includeDiff::
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 615accc..bec8a4b 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -639,6 +639,9 @@
 If link:config-gerrit.html#sendemail.allowrcpt[sendemail.allowrcpt] is
 configured, the added email address must belong to a domain that is
 allowed, unless `no_confirmation` is set.
+If link:config-gerrit.html#sendemail.denyrcpt[sendemail.denyrcpt]
+is configured, make sure that the added email address is *not* disallowed or
+belongs to a domain that is disallowed.
 
 The link:#email-input[EmailInput] object in the request body may
 contain additional options for the email address.
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 400bc4f..664c1bf 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -504,9 +504,7 @@
     if (addr != null && addr.getEmail() != null && addr.getEmail().length() > 0) {
       if (!args.validator.isValid(addr.getEmail())) {
         logger.atWarning().log("Not emailing %s (invalid email address)", addr.getEmail());
-      } else if (!args.emailSender.canEmail(addr.getEmail())) {
-        logger.atWarning().log("Not emailing %s (prohibited by allowrcpt)", addr.getEmail());
-      } else {
+      } else if (args.emailSender.canEmail(addr.getEmail())) {
         if (!smtpRcptTo.add(addr)) {
           if (!override) {
             return;
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 8615c04..a407cab 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
@@ -56,6 +57,8 @@
   /** The socket's connect timeout (0 = infinite timeout) */
   private static final int DEFAULT_CONNECT_TIMEOUT = 0;
 
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   public static class Module extends AbstractModule {
     @Override
     protected void configure() {
@@ -73,6 +76,7 @@
   private Encryption smtpEncryption;
   private boolean sslVerify;
   private Set<String> allowrcpt;
+  private Set<String> denyrcpt;
   private String importance;
   private int expiryDays;
 
@@ -117,6 +121,9 @@
     Set<String> rcpt = new HashSet<>();
     Collections.addAll(rcpt, cfg.getStringList("sendemail", null, "allowrcpt"));
     allowrcpt = Collections.unmodifiableSet(rcpt);
+    Set<String> rcptdeny = new HashSet<>();
+    Collections.addAll(rcptdeny, cfg.getStringList("sendemail", null, "denyrcpt"));
+    denyrcpt = Collections.unmodifiableSet(rcptdeny);
     importance = cfg.getString("sendemail", null, "importance");
     expiryDays = cfg.getInt("sendemail", null, "expiryDays", 0);
   }
@@ -129,22 +136,47 @@
   @Override
   public boolean canEmail(String address) {
     if (!isEnabled()) {
+      logger.atWarning().log("Not emailing %s (email is disabled)", address);
       return false;
     }
 
+    String domain = address.substring(address.lastIndexOf('@') + 1);
+    if (isDenied(address, domain)) {
+      return false;
+    }
+
+    return isAllowed(address, domain);
+  }
+
+  private boolean isDenied(String address, String domain) {
+
+    if (denyrcpt.isEmpty()) {
+      return false;
+    }
+
+    if (denyrcpt.contains(address)
+        || denyrcpt.contains(domain)
+        || denyrcpt.contains("@" + domain)) {
+      logger.atWarning().log("Not emailing %s (prohibited by sendemail.denyrcpt)", address);
+      return true;
+    }
+
+    return false;
+  }
+
+  private boolean isAllowed(String address, String domain) {
+
     if (allowrcpt.isEmpty()) {
       return true;
     }
 
-    if (allowrcpt.contains(address)) {
+    if (allowrcpt.contains(address)
+        || allowrcpt.contains(domain)
+        || allowrcpt.contains("@" + domain)) {
       return true;
     }
 
-    String domain = address.substring(address.lastIndexOf('@') + 1);
-    if (allowrcpt.contains(domain) || allowrcpt.contains("@" + domain)) {
-      return true;
-    }
-
+    logger.atWarning().log("Not emailing %s (prohibited by sendemail.allowrcpt)", address);
     return false;
   }