Send notification emails when inbound emails are rejected

With this commit, explanatory messages are sent when inbound messages
are rejected.
Four error types are defined:
PARSING_ERROR, INACTIVE_ACCOUNT, UNKNOWN_ACCOUNT, and INTERNAL_EXCEPTION.

PARSING_ERROR (probably the most frequent one) occurs when the Gerrit
metadatas can't be parsed.

INACTIVE_ACCOUNT occurs when the user's account is Inactive.

UNKNOWN_ACCOUNT occurs when zero or more than one accounts are found for
the incoming email address. This might be caused by multiple sources
providing the same user.

INTERNAL_EXCEPTION is used for all the other exceptions that can't be
described properly to the end user. For now, it is only fired when two
Changes are found with the same Id. This _should_ be a rare exception,
but it might occur.

This change also introduces a new MailHeader enum, and removes the
old MetadataName one. This allows for a cleaner way to define both Mail
headers and Gerrit "internal" metadata names.

Feature: Issue 8210
Change-Id: I48a081f2ce1be391b9f3ff991760740d5ada3357
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 2ee5eef..aa6fbfa 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1508,7 +1508,7 @@
     assertThat(m.rcpt()).containsExactly(expected);
     assertThat(((EmailHeader.AddressList) m.headers().get("To")).getAddressList())
         .containsExactly(expected);
-    assertThat(m.headers().get("CC").isEmpty()).isTrue();
+    assertThat(m.headers().get("Cc").isEmpty()).isTrue();
   }
 
   protected void assertNotifyCc(TestAccount expected) {
@@ -1520,7 +1520,7 @@
     Message m = sender.getMessages().get(0);
     assertThat(m.rcpt()).containsExactly(expected);
     assertThat(m.headers().get("To").isEmpty()).isTrue();
-    assertThat(((EmailHeader.AddressList) m.headers().get("CC")).getAddressList())
+    assertThat(((EmailHeader.AddressList) m.headers().get("Cc")).getAddressList())
         .containsExactly(expected);
   }
 
@@ -1529,7 +1529,7 @@
     Message m = sender.getMessages().get(0);
     assertThat(m.rcpt()).containsExactly(expected.emailAddress);
     assertThat(m.headers().get("To").isEmpty()).isTrue();
-    assertThat(m.headers().get("CC").isEmpty()).isTrue();
+    assertThat(m.headers().get("Cc").isEmpty()).isTrue();
   }
 
   protected interface ProjectWatchInfoConfiguration {
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 8333005..086cacb 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -113,7 +113,7 @@
       }
       recipients = new HashMap<>();
       recipients.put(TO, parseAddresses(message, "To"));
-      recipients.put(CC, parseAddresses(message, "CC"));
+      recipients.put(CC, parseAddresses(message, "Cc"));
       recipients.put(
           BCC,
           message
diff --git a/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index be61061..bc562cc 100644
--- a/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -121,6 +121,8 @@
     extractMailExample("Footer.soy");
     extractMailExample("FooterHtml.soy");
     extractMailExample("HeaderHtml.soy");
+    extractMailExample("InboundEmailRejection.soy");
+    extractMailExample("InboundEmailRejectionHtml.soy");
     extractMailExample("Merged.soy");
     extractMailExample("MergedHtml.soy");
     extractMailExample("NewChange.soy");
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index cbbccda..77312be 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -136,6 +136,7 @@
 import com.google.gerrit.server.git.validators.UploadValidationListener;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+import com.google.gerrit.server.mail.AutoReplyMailFilter;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.ListMailFilter;
 import com.google.gerrit.server.mail.MailFilter;
@@ -145,6 +146,7 @@
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
 import com.google.gerrit.server.mail.send.FromAddressGenerator;
 import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider;
+import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
 import com.google.gerrit.server.mail.send.MailSoyTofuProvider;
 import com.google.gerrit.server.mail.send.MailTemplates;
 import com.google.gerrit.server.mail.send.MergedSender;
@@ -265,6 +267,7 @@
     factory(RegisterNewEmailSender.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
     factory(SetAssigneeSender.Factory.class);
+    factory(InboundEmailRejectionSender.Factory.class);
     bind(PermissionCollection.Factory.class);
     bind(AccountVisibility.class).toProvider(AccountVisibilityProvider.class).in(SINGLETON);
     factory(ProjectOwnerGroupsProvider.Factory.class);
@@ -391,6 +394,9 @@
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
+    bind(AutoReplyMailFilter.class)
+        .annotatedWith(Exports.named("AutoReplyMailFilter"))
+        .to(AutoReplyMailFilter.class);
 
     factory(UploadValidators.Factory.class);
     DynamicSet.setOf(binder(), UploadValidationListener.class);
diff --git a/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java b/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
new file mode 100644
index 0000000..481c2e9
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2018 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.server.mail.receive.MailMessage;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Filters out auto-reply messages according to RFC 3834. */
+@Singleton
+public class AutoReplyMailFilter implements MailFilter {
+
+  private static final Logger log = LoggerFactory.getLogger(AutoReplyMailFilter.class);
+
+  @Override
+  public boolean shouldProcessMessage(MailMessage message) {
+    for (String header : message.additionalHeaders()) {
+      if (header.startsWith(MailHeader.PRECEDENCE.fieldWithDelimiter())) {
+        String prec = header.substring(MailHeader.PRECEDENCE.fieldWithDelimiter().length()).trim();
+
+        if (prec.equals("list") || prec.equals("junk") || prec.equals("bulk")) {
+          log.error(
+              "Message %s has a Precedence header. Will ignore and delete message.", message.id());
+          return false;
+        }
+
+      } else if (header.startsWith(MailHeader.AUTO_SUBMITTED.fieldWithDelimiter())) {
+        String autoSubmitted =
+            header.substring(MailHeader.AUTO_SUBMITTED.fieldWithDelimiter().length()).trim();
+
+        if (!autoSubmitted.equals("no")) {
+          log.error(
+              "Message %s has an Auto-Submitted header. Will ignore and delete message.",
+              message.id());
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/MailHeader.java b/java/com/google/gerrit/server/mail/MailHeader.java
new file mode 100644
index 0000000..be12288
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/MailHeader.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2018 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;
+
+/** Variables used by emails to hold data */
+public enum MailHeader {
+  // Gerrit metadata holders
+  ASSIGNEE("Gerrit-Assignee"),
+  BRANCH("Gerrit-Branch"),
+  CC("Gerit-CC"),
+  COMMENT_IN_REPLY_TO("Comment-In-Reply-To"),
+  COMMENT_DATE("Gerrit-Comment-Date"),
+  CHANGE_ID("Gerrit-Change-Id"),
+  CHANGE_NUMBER("Gerrit-Change-Number"),
+  CHANGE_URL("Gerrit-ChangeURL"),
+  COMMIT("Gerrit-Commit"),
+  HAS_COMMENTS("Gerrit-HasComments"),
+  HAS_LABELS("Gerrit-Has-Labels"),
+  MESSAGE_TYPE("Gerrit-MessageType"),
+  OWNER("Gerrit-Owner"),
+  PATCH_SET("Gerrit-PatchSet"),
+  PROJECT("Gerrit-Project"),
+  REVIEWER("Gerrit-Reviewer"),
+
+  // Commonly used Email headers
+  AUTO_SUBMITTED("Auto-Submitted"),
+  PRECEDENCE("Precedence"),
+  REFERENCES("References");
+
+  private final String name;
+  private final String fieldName;
+
+  MailHeader(String name) {
+    boolean customHeader = name.startsWith("Gerrit-");
+    this.name = name;
+
+    if (customHeader) {
+      this.fieldName = "X-" + name;
+    } else {
+      this.fieldName = name;
+    }
+  }
+
+  public String fieldWithDelimiter() {
+    return fieldName() + ": ";
+  }
+
+  public String withDelimiter() {
+    return name + ": ";
+  }
+
+  public String fieldName() {
+    return fieldName;
+  }
+
+  public String getName() {
+    return name;
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/MetadataName.java b/java/com/google/gerrit/server/mail/MetadataName.java
deleted file mode 100644
index 3080e4f..0000000
--- a/java/com/google/gerrit/server/mail/MetadataName.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2016 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;
-
-public final class MetadataName {
-  public static final String CHANGE_NUMBER = "Gerrit-Change-Number";
-  public static final String PATCH_SET = "Gerrit-PatchSet";
-  public static final String MESSAGE_TYPE = "Gerrit-MessageType";
-  public static final String TIMESTAMP = "Gerrit-Comment-Date";
-
-  public static String toHeader(String metadataName) {
-    return "X-" + metadataName;
-  }
-
-  public static String toHeaderWithDelimiter(String metadataName) {
-    return toHeader(metadataName) + ": ";
-  }
-
-  public static String toFooterWithDelimiter(String metadataName) {
-    return metadataName + ": ";
-  }
-}
diff --git a/java/com/google/gerrit/server/mail/receive/MetadataParser.java b/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
similarity index 65%
rename from java/com/google/gerrit/server/mail/receive/MetadataParser.java
rename to java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
index 9d7f087..05525bd 100644
--- a/java/com/google/gerrit/server/mail/receive/MetadataParser.java
+++ b/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
@@ -14,14 +14,11 @@
 
 package com.google.gerrit.server.mail.receive;
 
-import static com.google.gerrit.server.mail.MetadataName.toFooterWithDelimiter;
-import static com.google.gerrit.server.mail.MetadataName.toHeaderWithDelimiter;
-
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.MailUtil;
-import com.google.gerrit.server.mail.MetadataName;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.time.format.DateTimeParseException;
@@ -29,8 +26,8 @@
 import org.slf4j.LoggerFactory;
 
 /** Parse metadata from inbound email */
-public class MetadataParser {
-  private static final Logger log = LoggerFactory.getLogger(MetadataParser.class);
+public class MailHeaderParser {
+  private static final Logger log = LoggerFactory.getLogger(MailHeaderParser.class);
 
   public static MailMetadata parse(MailMessage m) {
     MailMetadata metadata = new MailMetadata();
@@ -39,22 +36,22 @@
 
     // Check email headers for X-Gerrit-<Name>
     for (String header : m.additionalHeaders()) {
-      if (header.startsWith(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER))) {
-        String num = header.substring(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER).length());
+      if (header.startsWith(MailHeader.CHANGE_NUMBER.fieldWithDelimiter())) {
+        String num = header.substring(MailHeader.CHANGE_NUMBER.fieldWithDelimiter().length());
         metadata.changeNumber = Ints.tryParse(num);
-      } else if (header.startsWith(toHeaderWithDelimiter(MetadataName.PATCH_SET))) {
-        String ps = header.substring(toHeaderWithDelimiter(MetadataName.PATCH_SET).length());
+      } else if (header.startsWith(MailHeader.PATCH_SET.fieldWithDelimiter())) {
+        String ps = header.substring(MailHeader.PATCH_SET.fieldWithDelimiter().length());
         metadata.patchSet = Ints.tryParse(ps);
-      } else if (header.startsWith(toHeaderWithDelimiter(MetadataName.TIMESTAMP))) {
-        String ts = header.substring(toHeaderWithDelimiter(MetadataName.TIMESTAMP).length()).trim();
+      } else if (header.startsWith(MailHeader.COMMENT_DATE.fieldWithDelimiter())) {
+        String ts = header.substring(MailHeader.COMMENT_DATE.fieldWithDelimiter().length()).trim();
         try {
           metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
         } catch (DateTimeParseException e) {
           log.error("Mail: Error while parsing timestamp from header of message " + m.id(), e);
         }
-      } else if (header.startsWith(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE))) {
+      } else if (header.startsWith(MailHeader.MESSAGE_TYPE.fieldWithDelimiter())) {
         metadata.messageType =
-            header.substring(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE).length());
+            header.substring(MailHeader.MESSAGE_TYPE.fieldWithDelimiter().length());
       }
     }
     if (metadata.hasRequiredFields()) {
@@ -85,22 +82,21 @@
 
   private static void extractFooters(Iterable<String> lines, MailMetadata metadata, MailMessage m) {
     for (String line : lines) {
-      if (metadata.changeNumber == null && line.contains(MetadataName.CHANGE_NUMBER)) {
+      if (metadata.changeNumber == null && line.contains(MailHeader.CHANGE_NUMBER.getName())) {
         metadata.changeNumber =
-            Ints.tryParse(extractFooter(toFooterWithDelimiter(MetadataName.CHANGE_NUMBER), line));
-      } else if (metadata.patchSet == null && line.contains(MetadataName.PATCH_SET)) {
+            Ints.tryParse(extractFooter(MailHeader.CHANGE_NUMBER.withDelimiter(), line));
+      } else if (metadata.patchSet == null && line.contains(MailHeader.PATCH_SET.getName())) {
         metadata.patchSet =
-            Ints.tryParse(extractFooter(toFooterWithDelimiter(MetadataName.PATCH_SET), line));
-      } else if (metadata.timestamp == null && line.contains(MetadataName.TIMESTAMP)) {
-        String ts = extractFooter(toFooterWithDelimiter(MetadataName.TIMESTAMP), line);
+            Ints.tryParse(extractFooter(MailHeader.PATCH_SET.withDelimiter(), line));
+      } else if (metadata.timestamp == null && line.contains(MailHeader.COMMENT_DATE.getName())) {
+        String ts = extractFooter(MailHeader.COMMENT_DATE.withDelimiter(), line);
         try {
           metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
         } catch (DateTimeParseException e) {
           log.error("Mail: Error while parsing timestamp from footer of message " + m.id(), e);
         }
-      } else if (metadata.messageType == null && line.contains(MetadataName.MESSAGE_TYPE)) {
-        metadata.messageType =
-            extractFooter(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE), line);
+      } else if (metadata.messageType == null && line.contains(MailHeader.MESSAGE_TYPE.getName())) {
+        metadata.messageType = extractFooter(MailHeader.MESSAGE_TYPE.withDelimiter(), line);
       }
     }
   }
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 3f794e8d0..9917261 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.mail.MailFilter;
+import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -77,6 +78,7 @@
   private static final Logger log = LoggerFactory.getLogger(MailProcessor.class);
 
   private final Emails emails;
+  private final InboundEmailRejectionSender.Factory emailRejectionSender;
   private final RetryHelper retryHelper;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final CommentsUtil commentsUtil;
@@ -94,6 +96,7 @@
   @Inject
   public MailProcessor(
       Emails emails,
+      InboundEmailRejectionSender.Factory emailRejectionSender,
       RetryHelper retryHelper,
       ChangeMessagesUtil changeMessagesUtil,
       CommentsUtil commentsUtil,
@@ -108,6 +111,7 @@
       AccountCache accountCache,
       @CanonicalWebUrl Provider<String> canonicalUrl) {
     this.emails = emails;
+    this.emailRejectionSender = emailRejectionSender;
     this.retryHelper = retryHelper;
     this.changeMessagesUtil = changeMessagesUtil;
     this.commentsUtil = commentsUtil;
@@ -148,21 +152,29 @@
       }
     }
 
-    MailMetadata metadata = MetadataParser.parse(message);
+    MailMetadata metadata = MailHeaderParser.parse(message);
+
     if (!metadata.hasRequiredFields()) {
       log.error(
           String.format(
               "Message %s is missing required metadata, have %s. Will delete message.",
               message.id(), metadata));
+      sendRejectionEmail(message, InboundEmailRejectionSender.Error.PARSING_ERROR);
       return;
     }
 
     Set<Account.Id> accountIds = emails.getAccountFor(metadata.author);
+
     if (accountIds.size() != 1) {
       log.error(
           String.format(
               "Address %s could not be matched to a unique account. It was matched to %s. Will delete message.",
               metadata.author, accountIds));
+
+      // We don't want to send an email if no accounts are linked to it.
+      if (accountIds.size() > 1) {
+        sendRejectionEmail(message, InboundEmailRejectionSender.Error.UNKNOWN_ACCOUNT);
+      }
       return;
     }
     Account.Id accountId = accountIds.iterator().next();
@@ -173,12 +185,23 @@
     }
     if (!accountState.get().getAccount().isActive()) {
       log.warn(String.format("Mail: Account %s is inactive. Will delete message.", accountId));
+      sendRejectionEmail(message, InboundEmailRejectionSender.Error.INACTIVE_ACCOUNT);
       return;
     }
 
     persistComments(buf, message, metadata, accountId);
   }
 
+  private void sendRejectionEmail(MailMessage message, InboundEmailRejectionSender.Error reason) {
+    try {
+      InboundEmailRejectionSender em =
+          emailRejectionSender.create(message.from(), message.id(), reason);
+      em.send();
+    } catch (Exception e) {
+      log.error("Cannot send email to warn for an error", e);
+    }
+  }
+
   private void persistComments(
       BatchUpdate.Factory buf, MailMessage message, MailMetadata metadata, Account.Id sender)
       throws OrmException, UpdateException, RestApiException {
@@ -190,6 +213,8 @@
             String.format(
                 "Message %s references unique change %s, but there are %d matching changes in the index. Will delete message.",
                 message.id(), metadata.changeNumber, changeDataList.size()));
+
+        sendRejectionEmail(message, InboundEmailRejectionSender.Error.INTERNAL_EXCEPTION);
         return;
       }
       ChangeData cd = changeDataList.get(0);
@@ -221,6 +246,7 @@
         log.warn(
             String.format(
                 "Could not parse any comments from %s. Will delete message.", message.id()));
+        sendRejectionEmail(message, InboundEmailRejectionSender.Error.PARSING_ERROR);
         return;
       }
 
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index cad15fa..29fc04f 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchList;
@@ -56,6 +57,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
+import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Repository;
@@ -158,7 +160,7 @@
     }
 
     if (patchSet != null) {
-      setHeader("X-Gerrit-PatchSet", patchSet.getPatchSetId() + "");
+      setHeader(MailHeader.PATCH_SET.fieldName(), patchSet.getPatchSetId() + "");
       if (patchSetInfo == null) {
         try {
           patchSetInfo =
@@ -178,11 +180,11 @@
 
     super.init();
     if (timestamp != null) {
-      setHeader("Date", new Date(timestamp.getTime()));
+      setHeader(FieldName.DATE, new Date(timestamp.getTime()));
     }
     setChangeSubjectHeader();
-    setHeader("X-Gerrit-Change-Id", "" + change.getKey().get());
-    setHeader("X-Gerrit-Change-Number", "" + change.getChangeId());
+    setHeader(MailHeader.CHANGE_ID.fieldName(), "" + change.getKey().get());
+    setHeader(MailHeader.CHANGE_NUMBER.fieldName(), "" + change.getChangeId());
     setChangeUrlHeader();
     setCommitIdHeader();
 
@@ -202,7 +204,7 @@
   private void setChangeUrlHeader() {
     final String u = getChangeUrl();
     if (u != null) {
-      setHeader("X-Gerrit-ChangeURL", "<" + u + ">");
+      setHeader(MailHeader.CHANGE_URL.fieldName(), "<" + u + ">");
     }
   }
 
@@ -211,12 +213,12 @@
         && patchSet.getRevision() != null
         && patchSet.getRevision().get() != null
         && patchSet.getRevision().get().length() > 0) {
-      setHeader("X-Gerrit-Commit", patchSet.getRevision().get());
+      setHeader(MailHeader.COMMIT.fieldName(), patchSet.getRevision().get());
     }
   }
 
   private void setChangeSubjectHeader() {
-    setHeader("Subject", textTemplate("ChangeSubject"));
+    setHeader(FieldName.SUBJECT, textTemplate("ChangeSubject"));
   }
 
   /** Get a link to the change; null if the server doesn't know its own address. */
@@ -481,19 +483,18 @@
     patchSetInfoData.put("authorEmail", patchSetInfo.getAuthor().getEmail());
     soyContext.put("patchSetInfo", patchSetInfoData);
 
-    footers.add("Gerrit-MessageType: " + messageClass);
-    footers.add("Gerrit-Change-Id: " + change.getKey().get());
-    footers.add("Gerrit-Change-Number: " + Integer.toString(change.getChangeId()));
-    footers.add("Gerrit-PatchSet: " + patchSet.getPatchSetId());
-    footers.add("Gerrit-Owner: " + getNameEmailFor(change.getOwner()));
+    footers.add(MailHeader.CHANGE_ID.withDelimiter() + change.getKey().get());
+    footers.add(MailHeader.CHANGE_NUMBER.withDelimiter() + Integer.toString(change.getChangeId()));
+    footers.add(MailHeader.PATCH_SET.withDelimiter() + patchSet.getPatchSetId());
+    footers.add(MailHeader.OWNER.withDelimiter() + getNameEmailFor(change.getOwner()));
     if (change.getAssignee() != null) {
-      footers.add("Gerrit-Assignee: " + getNameEmailFor(change.getAssignee()));
+      footers.add(MailHeader.ASSIGNEE.withDelimiter() + getNameEmailFor(change.getAssignee()));
     }
     for (String reviewer : getEmailsByState(ReviewerStateInternal.REVIEWER)) {
-      footers.add("Gerrit-Reviewer: " + reviewer);
+      footers.add(MailHeader.REVIEWER.withDelimiter() + reviewer);
     }
     for (String reviewer : getEmailsByState(ReviewerStateInternal.CC)) {
-      footers.add("Gerrit-CC: " + reviewer);
+      footers.add(MailHeader.CC.withDelimiter() + reviewer);
     }
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 5df0d62..b04dcd6 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.MailUtil;
 import com.google.gerrit.server.mail.receive.Protocol;
 import com.google.gerrit.server.patch.PatchFile;
@@ -54,6 +55,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
@@ -163,14 +165,14 @@
 
     // Add header that enables identifying comments on parsed email.
     // Grouping is currently done by timestamp.
-    setHeader("X-Gerrit-Comment-Date", timestamp);
+    setHeader(MailHeader.COMMENT_DATE.fieldName(), timestamp);
 
     if (incomingEmailEnabled) {
       if (replyToAddress == null) {
         // Remove Reply-To and use outbound SMTP (default) instead.
-        removeHeader("Reply-To");
+        removeHeader(FieldName.REPLY_TO);
       } else {
-        setHeader("Reply-To", replyToAddress);
+        setHeader(FieldName.REPLY_TO, replyToAddress);
       }
     }
   }
@@ -523,12 +525,12 @@
     soyContext.put(
         "coverLetterBlocks", commentBlocksToSoyData(CommentFormatter.parse(getCoverLetter())));
 
-    footers.add("Gerrit-Comment-Date: " + getCommentTimestamp());
-    footers.add("Gerrit-HasComments: " + (hasComments ? "Yes" : "No"));
-    footers.add("Gerrit-HasLabels: " + (labels.isEmpty() ? "No" : "Yes"));
+    footers.add(MailHeader.COMMENT_DATE.withDelimiter() + getCommentTimestamp());
+    footers.add(MailHeader.HAS_COMMENTS.withDelimiter() + (hasComments ? "Yes" : "No"));
+    footers.add(MailHeader.HAS_LABELS.withDelimiter() + (labels.isEmpty() ? "No" : "Yes"));
 
     for (Account.Id account : getReplyAccounts()) {
-      footers.add("Gerrit-Comment-In-Reply-To: " + getNameEmailFor(account));
+      footers.add(MailHeader.COMMENT_IN_REPLY_TO.withDelimiter() + getNameEmailFor(account));
     }
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
new file mode 100644
index 0000000..1793e74
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2018 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.send;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.MailHeader;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** Send an email to inform users that parsing their inbound email failed. */
+public class InboundEmailRejectionSender extends OutgoingEmail {
+
+  /** Used by the templating system to determine what error message should be sent */
+  public enum Error {
+    PARSING_ERROR,
+    INACTIVE_ACCOUNT,
+    UNKNOWN_ACCOUNT,
+    INTERNAL_EXCEPTION;
+  }
+
+  public interface Factory {
+    InboundEmailRejectionSender create(Address to, String threadId, Error reason);
+  }
+
+  private final Address to;
+  private final Error reason;
+  private final String threadId;
+
+  @Inject
+  public InboundEmailRejectionSender(
+      EmailArguments ea, @Assisted Address to, @Assisted String threadId, @Assisted Error reason)
+      throws OrmException {
+    super(ea, "error");
+    this.to = checkNotNull(to);
+    this.threadId = checkNotNull(threadId);
+    this.reason = checkNotNull(reason);
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+    setListIdHeader();
+
+    add(RecipientType.TO, to);
+
+    if (!threadId.isEmpty()) {
+      setHeader(MailHeader.REFERENCES.fieldName(), "<" + threadId + ">");
+    }
+  }
+
+  private void setListIdHeader() {
+    // Set a reasonable list id so that filters can be used to sort messages
+    setHeader("List-Id", "<gerrit-noreply." + getGerritHost() + ">");
+    if (getSettingsUrl() != null) {
+      setHeader("List-Unsubscribe", "<" + getSettingsUrl() + ">");
+    }
+  }
+
+  @Override
+  protected void format() throws EmailException {
+    appendText(textTemplate("InboundEmailRejection_" + reason.name()));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("InboundEmailRejectionHtml_" + reason.name()));
+    }
+  }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    footers.add(MailHeader.MESSAGE_TYPE.withDelimiter() + messageClass);
+  }
+
+  @Override
+  protected boolean supportsHtml() {
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java b/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
index b267275..8d7df41 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
@@ -51,6 +51,8 @@
     "DeleteReviewerHtml.soy",
     "DeleteVote.soy",
     "DeleteVoteHtml.soy",
+    "InboundEmailRejection.soy",
+    "InboundEmailRejectionHtml.soy",
     "Footer.soy",
     "FooterHtml.soy",
     "HeaderHtml.soy",
@@ -58,6 +60,8 @@
     "MergedHtml.soy",
     "NewChange.soy",
     "NewChangeHtml.soy",
+    "NoReplyFooter.soy",
+    "NoReplyFooterHtml.soy",
     "Private.soy",
     "RegisterNewEmail.soy",
     "ReplacePatchSet.soy",
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 3fefac4..f657fb0 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gwtorm.server.OrmException;
 import java.util.HashMap;
@@ -115,7 +116,7 @@
     branchData.put("shortName", branch.getShortName());
     soyContext.put("branch", branchData);
 
-    footers.add("Gerrit-Project: " + branch.getParentKey().get());
+    footers.add(MailHeader.PROJECT.withDelimiter() + branch.getParentKey().get());
     footers.add("Gerrit-Branch: " + branch.getShortName());
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 2d5c13bd..b882089 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.send.EmailHeader.AddressList;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
@@ -49,6 +50,7 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.StringJoiner;
+import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.util.SystemReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -57,9 +59,6 @@
 public abstract class OutgoingEmail {
   private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);
 
-  private static final String HDR_TO = "To";
-  private static final String HDR_CC = "CC";
-
   protected String messageClass;
   private final HashSet<Account.Id> rcptTo = new HashSet<>();
   private final Map<String, EmailHeader> headers;
@@ -163,7 +162,7 @@
       // Set Reply-To only if it hasn't been set by a child class
       // Reply-To will already be populated for the message types where Gerrit supports
       // inbound email replies.
-      if (!headers.containsKey("Reply-To")) {
+      if (!headers.containsKey(FieldName.REPLY_TO)) {
         StringJoiner j = new StringJoiner(", ");
         if (fromId != null) {
           Address address = toAddress(fromId);
@@ -173,7 +172,7 @@
         }
         smtpRcptTo.stream().forEach(a -> j.add(a.getEmail()));
         smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.getEmail()));
-        setHeader("Reply-To", j.toString());
+        setHeader(FieldName.REPLY_TO, j.toString());
       }
 
       String textPart = textBody.toString();
@@ -208,13 +207,13 @@
         Map<String, EmailHeader> shallowCopy = new HashMap<>();
         shallowCopy.putAll(headers);
         // Remove To and Cc
-        shallowCopy.remove(HDR_TO);
-        shallowCopy.remove(HDR_CC);
+        shallowCopy.remove(FieldName.TO);
+        shallowCopy.remove(FieldName.CC);
         for (Address a : smtpRcptToPlaintextOnly) {
           // Add new To
           EmailHeader.AddressList to = new EmailHeader.AddressList();
           to.add(a);
-          shallowCopy.put(HDR_TO, to);
+          shallowCopy.put(FieldName.TO, to);
         }
         args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body);
       }
@@ -233,17 +232,19 @@
     setupSoyContext();
 
     smtpFromAddress = args.fromAddressGenerator.from(fromId);
-    setHeader("Date", new Date());
-    headers.put("From", new EmailHeader.AddressList(smtpFromAddress));
-    headers.put(HDR_TO, new EmailHeader.AddressList());
-    headers.put(HDR_CC, new EmailHeader.AddressList());
-    setHeader("Message-ID", "");
+    setHeader(FieldName.DATE, new Date());
+    headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress));
+    headers.put(FieldName.TO, new EmailHeader.AddressList());
+    headers.put(FieldName.CC, new EmailHeader.AddressList());
+    setHeader(FieldName.MESSAGE_ID, "");
+    setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
 
     for (RecipientType recipientType : accountsToNotify.keySet()) {
       add(recipientType, accountsToNotify.get(recipientType));
     }
 
-    setHeader("X-Gerrit-MessageType", messageClass);
+    setHeader(MailHeader.MESSAGE_TYPE.fieldName(), messageClass);
+    footers.add(MailHeader.MESSAGE_TYPE.withDelimiter() + messageClass);
     textBody = new StringBuilder();
     htmlBody = new StringBuilder();
 
@@ -500,15 +501,15 @@
           if (!override) {
             return;
           }
-          ((EmailHeader.AddressList) headers.get(HDR_TO)).remove(addr.getEmail());
-          ((EmailHeader.AddressList) headers.get(HDR_CC)).remove(addr.getEmail());
+          ((EmailHeader.AddressList) headers.get(FieldName.TO)).remove(addr.getEmail());
+          ((EmailHeader.AddressList) headers.get(FieldName.CC)).remove(addr.getEmail());
         }
         switch (rt) {
           case TO:
-            ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+            ((EmailHeader.AddressList) headers.get(FieldName.TO)).add(addr);
             break;
           case CC:
-            ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+            ((EmailHeader.AddressList) headers.get(FieldName.CC)).add(addr);
             break;
           case BCC:
             break;
diff --git a/java/com/google/gerrit/testing/FakeEmailSender.java b/java/com/google/gerrit/testing/FakeEmailSender.java
index 7aed684..19e85db 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.MailHeader;
 import com.google.gerrit.server.mail.send.EmailHeader;
 import com.google.gerrit.server.mail.send.EmailSender;
 import com.google.inject.AbstractModule;
@@ -148,8 +149,8 @@
   }
 
   public List<Message> getMessages(String changeId, String type) {
-    final String idFooter = "\nGerrit-Change-Id: " + changeId + "\n";
-    final String typeFooter = "\nGerrit-MessageType: " + type + "\n";
+    final String idFooter = "\n" + MailHeader.CHANGE_ID.withDelimiter() + changeId + "\n";
+    final String typeFooter = "\n" + MailHeader.MESSAGE_TYPE.withDelimiter() + type + "\n";
     return getMessages()
         .stream()
         .filter(in -> in.body().contains(idFooter) && in.body().contains(typeFooter))
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
index a96c6ec..df4076d 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
@@ -68,6 +68,9 @@
     // Check that the comments from the email have NOT been persisted
     Collection<ChangeMessageInfo> messages = gApi.changes().id(changeInfo.id).get().messages;
     assertThat(messages).hasSize(2);
+
+    // Check that no emails were sent because of this error
+    assertThat(sender.getMessages()).isEmpty();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 9de4797..71976f5 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.mail.MailUtil;
 import com.google.gerrit.server.mail.receive.MailMessage;
 import com.google.gerrit.server.mail.receive.MailProcessor;
+import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
@@ -226,4 +227,33 @@
 
     assertNotifyTo(admin);
   }
+
+  @Test
+  public void sendNotificationOnMissingMetadatas() throws Exception {
+    String changeId = createChangeWithReview();
+    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+    List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
+    assertThat(comments).hasSize(2);
+    String ts = "null"; // Erroneous timestamp to be used in erroneous metadatas
+
+    // Build Message
+    String txt =
+        newPlaintextBody(
+            canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
+            "Test Message",
+            null,
+            null,
+            null);
+    MailMessage.Builder b =
+        messageBuilderWithDefaultFields()
+            .from(user.emailAddress)
+            .textContent(txt + textFooterForChange(changeInfo._number, ts));
+
+    sender.clear();
+    mailProcessor.process(b.build());
+
+    assertNotifyTo(user);
+    Message message = sender.nextMessage();
+    assertThat(message.body()).contains("was unable to parse your email");
+  }
 }
diff --git a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
new file mode 100644
index 0000000..a7234f4
--- /dev/null
+++ b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2018 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.testing.GerritBaseTests;
+import java.time.Instant;
+import org.junit.Test;
+
+public class AutoReplyMailFilterTest extends GerritBaseTests {
+
+  private AutoReplyMailFilter autoReplyMailFilter = new AutoReplyMailFilter();
+
+  @Test
+  public void acceptsHumanReply() {
+    MailMessage.Builder b = createChangeAndReplyByEmail();
+    assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isTrue();
+  }
+
+  @Test
+  public void discardsBulk() {
+    MailMessage.Builder b = createChangeAndReplyByEmail();
+    b.addAdditionalHeader("Precedence: bulk");
+    assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isFalse();
+
+    b = createChangeAndReplyByEmail();
+    b.addAdditionalHeader("Precedence: list");
+    assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isFalse();
+
+    b = createChangeAndReplyByEmail();
+    b.addAdditionalHeader("Precedence: junk");
+    assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isFalse();
+  }
+
+  @Test
+  public void discardsAutoSubmitted() {
+    MailMessage.Builder b = createChangeAndReplyByEmail();
+    b.addAdditionalHeader("Auto-Submitted: yes");
+    assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isFalse();
+
+    b = createChangeAndReplyByEmail();
+    b.addAdditionalHeader("Auto-Submitted: no");
+    assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isTrue();
+  }
+
+  private MailMessage.Builder createChangeAndReplyByEmail() {
+    // Build Message
+    MailMessage.Builder b = MailMessage.builder();
+    b.id("some id");
+    b.from(new Address("admim@example.com"));
+    b.addTo(new Address("gerrit@my-company.com")); // Not evaluated
+    b.subject("");
+    b.dateReceived(Instant.now());
+    b.textContent("I am currently out of office, please leave a code review after the beep.");
+    return b;
+  }
+}
diff --git a/javatests/com/google/gerrit/server/mail/receive/MetadataParserTest.java b/javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java
similarity index 71%
rename from javatests/com/google/gerrit/server/mail/receive/MetadataParserTest.java
rename to javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java
index dc25939..b7277f3 100644
--- a/javatests/com/google/gerrit/server/mail/receive/MetadataParserTest.java
+++ b/javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java
@@ -15,18 +15,16 @@
 package com.google.gerrit.server.mail.receive;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.mail.MetadataName.toFooterWithDelimiter;
-import static com.google.gerrit.server.mail.MetadataName.toHeaderWithDelimiter;
 
 import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.MetadataName;
+import com.google.gerrit.server.mail.MailHeader;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
 import org.junit.Test;
 
-public class MetadataParserTest {
+public class MailHeaderParserTest {
   @Test
   public void parseMetadataFromHeader() {
     // This tests if the metadata parser is able to parse metadata from the
@@ -36,16 +34,16 @@
     b.dateReceived(Instant.now());
     b.subject("");
 
-    b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER) + "123");
-    b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.PATCH_SET) + "1");
-    b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment");
+    b.addAdditionalHeader(MailHeader.CHANGE_NUMBER.fieldWithDelimiter() + "123");
+    b.addAdditionalHeader(MailHeader.PATCH_SET.fieldWithDelimiter() + "1");
+    b.addAdditionalHeader(MailHeader.MESSAGE_TYPE.fieldWithDelimiter() + "comment");
     b.addAdditionalHeader(
-        toHeaderWithDelimiter(MetadataName.TIMESTAMP) + "Tue, 25 Oct 2016 02:11:35 -0700");
+        MailHeader.COMMENT_DATE.fieldWithDelimiter() + "Tue, 25 Oct 2016 02:11:35 -0700");
 
     Address author = new Address("Diffy", "test@gerritcodereview.com");
     b.from(author);
 
-    MailMetadata meta = MetadataParser.parse(b.build());
+    MailMetadata meta = MailHeaderParser.parse(b.build());
     assertThat(meta.author).isEqualTo(author.getEmail());
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
@@ -67,17 +65,17 @@
     b.subject("");
 
     StringBuilder stringBuilder = new StringBuilder();
-    stringBuilder.append(toFooterWithDelimiter(MetadataName.CHANGE_NUMBER) + "123\r\n");
-    stringBuilder.append("> " + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1\n");
-    stringBuilder.append(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment\n");
+    stringBuilder.append(MailHeader.CHANGE_NUMBER.withDelimiter() + "123\r\n");
+    stringBuilder.append("> " + MailHeader.PATCH_SET.withDelimiter() + "1\n");
+    stringBuilder.append(MailHeader.MESSAGE_TYPE.withDelimiter() + "comment\n");
     stringBuilder.append(
-        toFooterWithDelimiter(MetadataName.TIMESTAMP) + "Tue, 25 Oct 2016 02:11:35 -0700\r\n");
+        MailHeader.COMMENT_DATE.withDelimiter() + "Tue, 25 Oct 2016 02:11:35 -0700\r\n");
     b.textContent(stringBuilder.toString());
 
     Address author = new Address("Diffy", "test@gerritcodereview.com");
     b.from(author);
 
-    MailMetadata meta = MetadataParser.parse(b.build());
+    MailMetadata meta = MailHeaderParser.parse(b.build());
     assertThat(meta.author).isEqualTo(author.getEmail());
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
@@ -100,13 +98,12 @@
 
     StringBuilder stringBuilder = new StringBuilder();
     stringBuilder.append(
-        "<div id\"someid\">" + toFooterWithDelimiter(MetadataName.CHANGE_NUMBER) + "123</div>");
-    stringBuilder.append("<div>" + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1</div>");
-    stringBuilder.append(
-        "<div>" + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment</div>");
+        "<div id\"someid\">" + MailHeader.CHANGE_NUMBER.withDelimiter() + "123</div>");
+    stringBuilder.append("<div>" + MailHeader.PATCH_SET.withDelimiter() + "1</div>");
+    stringBuilder.append("<div>" + MailHeader.MESSAGE_TYPE.withDelimiter() + "comment</div>");
     stringBuilder.append(
         "<div>"
-            + toFooterWithDelimiter(MetadataName.TIMESTAMP)
+            + MailHeader.COMMENT_DATE.withDelimiter()
             + "Tue, 25 Oct 2016 02:11:35 -0700"
             + "</div>");
     b.htmlContent(stringBuilder.toString());
@@ -114,7 +111,7 @@
     Address author = new Address("Diffy", "test@gerritcodereview.com");
     b.from(author);
 
-    MailMetadata meta = MetadataParser.parse(b.build());
+    MailMetadata meta = MailHeaderParser.parse(b.build());
     assertThat(meta.author).isEqualTo(author.getEmail());
     assertThat(meta.changeNumber).isEqualTo(123);
     assertThat(meta.patchSet).isEqualTo(1);
diff --git a/resources/com/google/gerrit/server/mail/AddKey.soy b/resources/com/google/gerrit/server/mail/AddKey.soy
index af99569..be76aee 100644
--- a/resources/com/google/gerrit/server/mail/AddKey.soy
+++ b/resources/com/google/gerrit/server/mail/AddKey.soy
@@ -64,8 +64,5 @@
   browser window instead.
 
   {\n}
-  {\n}
-
-  This is a send-only email address.  Replies to this message will not be read
-  or answered.
+  {call .NoReplyFooter /}
 {/template}
diff --git a/resources/com/google/gerrit/server/mail/AddKeyHtml.soy b/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
index 712abc7..04a0635 100644
--- a/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
+++ b/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
@@ -59,8 +59,5 @@
     {/if}.
   </p>
 
-  <p>
-    This is a send-only email address.  Replies to this message will not be read
-    or answered.
-  </p>
+  {call .NoReplyFooterHtml /}
 {/template}
diff --git a/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy b/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
new file mode 100644
index 0000000..e997776
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
@@ -0,0 +1,64 @@
+/**
+ * Copyright (C) 2018 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .InboundEmailRejectionFooter kind="text"}
+  {\n}
+  {\n}
+  Thus, no actions were taken by Gerrit in response to this email,
+  and you should use the Gerrit website to continue.
+  {\n}
+  This email was sent in response to an email coming from this address.
+  In case you did not send Gerrit an email, feel free to ignore this.
+  {call .NoReplyFooter /}
+{/template}
+
+/**
+ * The .InboundEmailRejection templates will determine the contents of the email related
+ * to warning users of error in inbound emails
+ */
+
+{template .InboundEmailRejection_PARSING_ERROR kind="text"}
+  Gerrit Code Review was unable to parse your email.{\n}
+  This might be because your email did not quote Gerrit's email,
+  because you are using an unsupported email client,
+  or because of a bug.
+  {call .InboundEmailRejectionFooter /}
+{/template}
+
+{template .InboundEmailRejection_UNKNOWN_ACCOUNT kind="text"}
+  Gerrit Code Review was unable to match your email to an account.{\n}
+  This may happen if several accounts are linked to this email address.
+  {call .InboundEmailRejectionFooter /}
+{/template}
+
+{template .InboundEmailRejection_INACTIVE_ACCOUNT kind="text"}
+  Your account on this Gerrit Code Review instance is marked as inactive,
+  so your email has been ignored. {\n}
+  If you think this is an error, please contact your Gerrit instance administrator.
+  {\n}{\n}
+  This email was sent in response to an email coming from this address.
+  In case you did not send Gerrit an email, feel free to ignore this.
+  {call .NoReplyFooter /}
+{/template}
+
+{template .InboundEmailRejection_INTERNAL_EXCEPTION kind="text"}
+  Gerrit Code Review encountered an internal exception and was unable to fulfil your request.
+  {\n}
+  This might be caused by an ongoing maintenance or a data corruption.
+  {call .InboundEmailRejectionFooter /}
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy b/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
new file mode 100644
index 0000000..f879270
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
@@ -0,0 +1,80 @@
+/**
+ * Copyright (C) 2018 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+
+{template .InboundEmailRejectionFooterHtml}
+  <p>
+    Thus, no actions were taken by Gerrit in response to this email,
+    and you should use the Gerrit website to continue.
+  </p>
+  <p>
+    In case you did not send Gerrit an email, feel free to ignore this.
+  </p>
+  {call .NoReplyFooterHtml /}
+{/template}
+
+/**
+ * The .InboundEmailRejection templates will determine the contents of the email related
+ * to warning users of error in inbound emails
+ */
+
+{template .InboundEmailRejectionHtml_PARSING_ERROR}
+  <p>
+    Gerrit Code Review was unable to parse your email.
+  </p>
+  <p>
+    This might be because your email did not quote Gerrit's email,
+    because you are using an unsupported email client,
+    or because of a bug.
+  </p>
+  {call .InboundEmailRejectionFooterHtml /}
+{/template}
+
+{template .InboundEmailRejectionHtml_UNKNOWN_ACCOUNT}
+  <p>
+    Gerrit Code Review was unable to match your email to an account.
+  </p>
+  <p>
+    This may happen if several accounts are linked to this email address.
+  </p>
+  {call .InboundEmailRejectionFooterHtml /}
+{/template}
+
+{template .InboundEmailRejectionHtml_INACTIVE_ACCOUNT}
+  <p>
+    Your account on this Gerrit Code Review instance is marked as inactive,
+    so your email has been ignored.
+  </p>
+  <p>
+    If you think this is an error, please contact your Gerrit instance administrator.
+  </p>
+  <p>
+    In case you did not send Gerrit an email, feel free to ignore this.
+  </p>
+  {call .NoReplyFooter /}
+{/template}
+
+{template .InboundEmailRejectionHtml_INTERNAL_EXCEPTION}
+  <p>
+    Gerrit Code Review encountered an internal exception and was unable to fulfil your request.
+  </p>
+  <p>
+    This might be caused by an ongoing maintenance or a data corruption.
+  <p>
+  {call .InboundEmailRejectionFooterHtml /}
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/NoReplyFooter.soy b/resources/com/google/gerrit/server/mail/NoReplyFooter.soy
new file mode 100644
index 0000000..1443100
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/NoReplyFooter.soy
@@ -0,0 +1,23 @@
+/**
+ * Copyright (C) 2018 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .NoReplyFooter kind="text"}
+  {\n}
+  This is a send-only email address.  Replies to this message will not be read
+  or answered.
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/NoReplyFooterHtml.soy b/resources/com/google/gerrit/server/mail/NoReplyFooterHtml.soy
new file mode 100644
index 0000000..93df527
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/NoReplyFooterHtml.soy
@@ -0,0 +1,24 @@
+/**
+ * Copyright (C) 2018 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .NoReplyFooterHtml}
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}