Add email sending for attention set updates

Currently, we don't send any emails when updating the attention set.
This change adds email sendings by default to OWNER, when adding
users to the attention set or removing them from it, but only when
added/removed explictly. Explict removals are through the review
endpoint, or through the dedicated attention set endpoints.

All of the explict updates allow specifying AttentionSetInput,
and now it is also possible to specify notify and notify_details
to change the receivers of the attention set emails.

Change-Id: Ifca8e2dd5de92a608fa987ea894d6326d1e88135
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 8f20ac5..261b296 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6115,7 +6115,7 @@
 |Field Name    ||Description
 |`account`     || link:rest-api-accounts.html#account-info[AccountInfo] entity.
 |`last_update` || The link:rest-api.html#timestamp[timestamp] of the last update.
-|`reason`      | The reason of for adding or removing the user.
+|`reason`      || The reason of for adding or removing the user.
 
 |===========================
 [[attention-set-input]]
@@ -6125,11 +6125,19 @@
 
 [options="header",cols="1,^1,5"]
 |===========================
-|Field Name    ||Description
-|`user`        |optional| link:rest-api-accounts.html#account-id[ID]
+|Field Name        ||Description
+|`user`            |optional| link:rest-api-accounts.html#account-id[ID]
 of the account that should be added to the attention set. For removals,
 this field should be empty or the same as the field in the request header.
-|`reason`      | The reason of for adding or removing the user.
+|`reason`          || The reason of for adding or removing the user.
+|`notify`          |optional|
+Notify handling that defines to whom email notifications should be sent
+after the change is created. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `OWNER`.
+|`notify_details`  |optional|
+Additional information about whom to notify about the change creation
+as a map of recipient type to link:#notify-info[NotifyInfo] entity.
 |===========================
 
 [[blame-info]]
diff --git a/java/com/google/gerrit/extensions/api/changes/AttentionSetInput.java b/java/com/google/gerrit/extensions/api/changes/AttentionSetInput.java
index f0d42c5..4665b11 100644
--- a/java/com/google/gerrit/extensions/api/changes/AttentionSetInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/AttentionSetInput.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.common.AttentionSetInfo;
 import com.google.gerrit.extensions.restapi.DefaultInput;
+import java.util.Map;
 
 /**
  * Input at API level to add a user to the attention set.
@@ -25,6 +26,8 @@
 public class AttentionSetInput {
   public String user;
   @DefaultInput public String reason;
+  public NotifyHandling notify;
+  public Map<RecipientType, NotifyInfo> notifyDetails;
 
   public AttentionSetInput(String user, String reason) {
     this.user = user;
diff --git a/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index fff33e5..8a71c1c 100644
--- a/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -108,6 +108,8 @@
     extractMailExample("AbandonedHtml.soy");
     extractMailExample("AddKey.soy");
     extractMailExample("AddKeyHtml.soy");
+    extractMailExample("AddToAttentionSet.soy");
+    extractMailExample("AddToAttentionSetHtml.soy");
     extractMailExample("ChangeFooter.soy");
     extractMailExample("ChangeFooterHtml.soy");
     extractMailExample("ChangeSubject.soy");
@@ -134,6 +136,8 @@
     extractMailExample("NewChangeHtml.soy");
     extractMailExample("RegisterNewEmail.soy");
     extractMailExample("RegisterNewEmailHtml.soy");
+    extractMailExample("RemoveFromAttentionSet.soy");
+    extractMailExample("RemoveFromAttentionSetHtml.soy");
     extractMailExample("ReplacePatchSet.soy");
     extractMailExample("ReplacePatchSetHtml.soy");
     extractMailExample("Restored.soy");
diff --git a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
index 48de684..8053b30 100644
--- a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
@@ -16,43 +16,107 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.mail.send.AddToAttentionSetSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.util.AttentionSetEmail;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
 
 /** Add a specified user to the attention set. */
 public class AddToAttentionSetOp implements BatchUpdateOp {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    AddToAttentionSetOp create(Account.Id attentionUserId, String reason);
+    AddToAttentionSetOp create(Account.Id attentionUserId, String reason, boolean notify);
   }
 
+  private final ChangeData.Factory changeDataFactory;
+  private final MessageIdGenerator messageIdGenerator;
+  private final AddToAttentionSetSender.Factory addToAttentionSetSender;
+  private final AttentionSetEmail.Factory attentionSetEmailFactory;
+
   private final Account.Id attentionUserId;
   private final String reason;
 
+  private Change change;
+  private boolean notify;
+
   /**
    * Add a specified user to the attention set.
    *
    * @param attentionUserId the id of the user we want to add to the attention set.
-   * @param reason The reason for adding that user.
+   * @param reason the reason for adding that user.
+   * @param notify whether or not to send emails if the operation is successful.
    */
   @Inject
-  AddToAttentionSetOp(@Assisted Account.Id attentionUserId, @Assisted String reason) {
+  AddToAttentionSetOp(
+      ChangeData.Factory changeDataFactory,
+      AddToAttentionSetSender.Factory addToAttentionSetSender,
+      MessageIdGenerator messageIdGenerator,
+      AttentionSetEmail.Factory attentionSetEmailFactory,
+      @Assisted Account.Id attentionUserId,
+      @Assisted String reason,
+      @Assisted boolean notify) {
+    this.changeDataFactory = changeDataFactory;
+    this.addToAttentionSetSender = addToAttentionSetSender;
+    this.messageIdGenerator = messageIdGenerator;
+    this.attentionSetEmailFactory = attentionSetEmailFactory;
+
     this.attentionUserId = requireNonNull(attentionUserId, "user");
     this.reason = requireNonNull(reason, "reason");
+    this.notify = notify;
   }
 
   @Override
   public boolean updateChange(ChangeContext ctx) throws RestApiException {
+    ChangeData changeData = changeDataFactory.create(ctx.getNotes());
+    if (changeData.attentionSet().stream()
+        .anyMatch(
+            u ->
+                u.account().equals(attentionUserId)
+                    && u.operation() == AttentionSetUpdate.Operation.ADD)) {
+      // We still need to perform this update to ensure that we don't remove the user in a follow-up
+      // operation, but no need to send an email about it.
+      notify = false;
+    }
+
+    change = ctx.getChange();
+
     ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
     update.addToPlannedAttentionSetUpdates(
         AttentionSetUpdate.createForWrite(
             attentionUserId, AttentionSetUpdate.Operation.ADD, reason));
     return true;
   }
+
+  @Override
+  public void postUpdate(Context ctx) {
+    if (!notify) {
+      return;
+    }
+    try {
+      attentionSetEmailFactory
+          .create(
+              addToAttentionSetSender.create(ctx.getProject(), change.getId()),
+              ctx,
+              change,
+              reason,
+              messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()),
+              attentionUserId)
+          .sendAsync();
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log(e.getMessage(), change.getId());
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
index 6be1343..e532409 100644
--- a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
@@ -16,43 +16,107 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.AttentionSetUpdate.Operation;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.mail.send.RemoveFromAttentionSetSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.util.AttentionSetEmail;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Optional;
 
 /** Remove a specified user from the attention set. */
 public class RemoveFromAttentionSetOp implements BatchUpdateOp {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public interface Factory {
-    RemoveFromAttentionSetOp create(Account.Id attentionUserId, String reason);
+    RemoveFromAttentionSetOp create(Account.Id attentionUserId, String reason, boolean notify);
   }
 
+  private final ChangeData.Factory changeDataFactory;
+  private final MessageIdGenerator messageIdGenerator;
+  private final RemoveFromAttentionSetSender.Factory removeFromAttentionSetSender;
+  private final AttentionSetEmail.Factory attentionSetEmailFactory;
+
   private final Account.Id attentionUserId;
   private final String reason;
 
+  private Change change;
+  private boolean notify;
+
   /**
    * Remove a specified user from the attention set.
    *
    * @param attentionUserId the id of the user we want to add to the attention set.
-   * @param reason The reason for adding that user.
+   * @param reason the reason for adding that user.
+   * @param notify whether or not to send emails if the operation is successful.
    */
   @Inject
-  RemoveFromAttentionSetOp(@Assisted Account.Id attentionUserId, @Assisted String reason) {
+  RemoveFromAttentionSetOp(
+      ChangeData.Factory changeDataFactory,
+      MessageIdGenerator messageIdGenerator,
+      RemoveFromAttentionSetSender.Factory removeFromAttentionSetSenderFactory,
+      AttentionSetEmail.Factory attentionSetEmailFactory,
+      @Assisted Account.Id attentionUserId,
+      @Assisted String reason,
+      @Assisted boolean notify) {
+    this.changeDataFactory = changeDataFactory;
+    this.messageIdGenerator = messageIdGenerator;
+    this.removeFromAttentionSetSender = removeFromAttentionSetSenderFactory;
+    this.attentionSetEmailFactory = attentionSetEmailFactory;
     this.attentionUserId = requireNonNull(attentionUserId, "user");
     this.reason = requireNonNull(reason, "reason");
+    this.notify = notify;
   }
 
   @Override
   public boolean updateChange(ChangeContext ctx) throws RestApiException {
+    ChangeData changeData = changeDataFactory.create(ctx.getNotes());
+    Optional<AttentionSetUpdate> existingEntry =
+        changeData.attentionSet().stream()
+            .filter(u -> u.account().equals(attentionUserId))
+            .findAny();
+    if (!existingEntry.isPresent() || existingEntry.get().operation() == Operation.REMOVE) {
+      // We still need to perform this update to ensure that we don't add the user in a follow-up
+      // operation, but no need to send an email about it.
+      notify = false;
+    }
+
+    change = ctx.getChange();
+
     ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
     update.addToPlannedAttentionSetUpdates(
         AttentionSetUpdate.createForWrite(attentionUserId, Operation.REMOVE, reason));
     return true;
   }
+
+  @Override
+  public void postUpdate(Context ctx) {
+    if (!notify) {
+      return;
+    }
+    try {
+      attentionSetEmailFactory
+          .create(
+              removeFromAttentionSetSender.create(ctx.getProject(), change.getId()),
+              ctx,
+              change,
+              reason,
+              messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()),
+              attentionUserId)
+          .sendAsync();
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log(e.getMessage(), change.getId());
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/mail/EmailModule.java b/java/com/google/gerrit/server/mail/EmailModule.java
index cc3db75..ff166b1 100644
--- a/java/com/google/gerrit/server/mail/EmailModule.java
+++ b/java/com/google/gerrit/server/mail/EmailModule.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.server.mail.send.AbandonedSender;
 import com.google.gerrit.server.mail.send.AddKeySender;
 import com.google.gerrit.server.mail.send.AddReviewerSender;
+import com.google.gerrit.server.mail.send.AddToAttentionSetSender;
 import com.google.gerrit.server.mail.send.CommentSender;
 import com.google.gerrit.server.mail.send.CreateChangeSender;
 import com.google.gerrit.server.mail.send.DeleteKeySender;
@@ -26,6 +27,7 @@
 import com.google.gerrit.server.mail.send.HttpPasswordUpdateSender;
 import com.google.gerrit.server.mail.send.MergedSender;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
+import com.google.gerrit.server.mail.send.RemoveFromAttentionSetSender;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.mail.send.RestoredSender;
 import com.google.gerrit.server.mail.send.RevertedSender;
@@ -49,5 +51,7 @@
     factory(RestoredSender.Factory.class);
     factory(RevertedSender.Factory.class);
     factory(SetAssigneeSender.Factory.class);
+    factory(AddToAttentionSetSender.Factory.class);
+    factory(RemoveFromAttentionSetSender.Factory.class);
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/AddToAttentionSetSender.java b/java/com/google/gerrit/server/mail/send/AddToAttentionSetSender.java
new file mode 100644
index 0000000..b13bcf6
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/AddToAttentionSetSender.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 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 com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** Let users know of a new user in the attention set. */
+public class AddToAttentionSetSender extends AttentionSetSender {
+
+  public interface Factory extends ReplyToChangeSender.Factory<AddToAttentionSetSender> {
+    @Override
+    AddToAttentionSetSender create(Project.NameKey project, Change.Id changeId);
+  }
+
+  @Inject
+  public AddToAttentionSetSender(
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, project, changeId);
+  }
+
+  @Override
+  protected void formatChange() throws EmailException {
+    appendText(textTemplate("AddToAttentionSet"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("AddToAttentionSetHtml"));
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/AttentionSetSender.java b/java/com/google/gerrit/server/mail/send/AttentionSetSender.java
new file mode 100644
index 0000000..8f898a8
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/AttentionSetSender.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2020 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 com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.EmailException;
+
+/** Base class for Attention Set email senders */
+public abstract class AttentionSetSender extends ReplyToChangeSender {
+  private Account.Id attentionSetUser;
+  private String reason;
+
+  public AttentionSetSender(EmailArguments args, Project.NameKey project, Change.Id changeId) {
+    super(args, "addToAttentionSet", ChangeEmail.newChangeData(args, project, changeId));
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+
+    ccAllApprovals();
+    bccStarredBy();
+    ccExistingReviewers();
+    removeUsersThatIgnoredTheChange();
+  }
+
+  public void setAttentionSetUser(Account.Id attentionSetUser) {
+    this.attentionSetUser = attentionSetUser;
+  }
+
+  public void setReason(String reason) {
+    this.reason = reason;
+  }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContext.put("attentionSetUser", getNameFor(attentionSetUser));
+    soyContext.put("reason", reason);
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java b/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
index 07ca254..623bdc2 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
@@ -41,6 +41,8 @@
     "AbandonedHtml.soy",
     "AddKey.soy",
     "AddKeyHtml.soy",
+    "AddToAttentionSet.soy",
+    "AddToAttentionSetHtml.soy",
     "ChangeFooter.soy",
     "ChangeFooterHtml.soy",
     "ChangeSubject.soy",
@@ -70,6 +72,8 @@
     "Private.soy",
     "RegisterNewEmail.soy",
     "RegisterNewEmailHtml.soy",
+    "RemoveFromAttentionSet.soy",
+    "RemoveFromAttentionSetHtml.soy",
     "ReplacePatchSet.soy",
     "ReplacePatchSetHtml.soy",
     "Restored.soy",
diff --git a/java/com/google/gerrit/server/mail/send/RemoveFromAttentionSetSender.java b/java/com/google/gerrit/server/mail/send/RemoveFromAttentionSetSender.java
new file mode 100644
index 0000000..6762b7d
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/RemoveFromAttentionSetSender.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2020 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 com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** Let users know of a user removed from the attention set. */
+public class RemoveFromAttentionSetSender extends AttentionSetSender {
+
+  public interface Factory extends ReplyToChangeSender.Factory<RemoveFromAttentionSetSender> {
+    @Override
+    RemoveFromAttentionSetSender create(Project.NameKey project, Change.Id changeId);
+  }
+
+  @Inject
+  public RemoveFromAttentionSetSender(
+      EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+    super(args, project, changeId);
+  }
+
+  @Override
+  protected void formatChange() throws EmailException {
+    appendText(textTemplate("RemoveFromAttentionSet"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("RemoveFromAttentionSetHtml"));
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
index a8b65bd..3ecf67f6 100644
--- a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
+++ b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -25,6 +26,7 @@
 import com.google.gerrit.server.change.AddToAttentionSetOp;
 import com.google.gerrit.server.change.AttentionSetEntryResource;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -43,6 +45,7 @@
   private final AddToAttentionSetOp.Factory opFactory;
   private final AccountLoader.Factory accountLoaderFactory;
   private final PermissionBackend permissionBackend;
+  private final NotifyResolver notifyResolver;
 
   @Inject
   AddToAttentionSet(
@@ -50,12 +53,14 @@
       AccountResolver accountResolver,
       AddToAttentionSetOp.Factory opFactory,
       AccountLoader.Factory accountLoaderFactory,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      NotifyResolver notifyResolver) {
     this.updateFactory = updateFactory;
     this.accountResolver = accountResolver;
     this.opFactory = opFactory;
     this.accountLoaderFactory = accountLoaderFactory;
     this.permissionBackend = permissionBackend;
+    this.notifyResolver = notifyResolver;
   }
 
   @Override
@@ -76,8 +81,11 @@
     try (BatchUpdate bu =
         updateFactory.create(
             changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
-      AddToAttentionSetOp op = opFactory.create(attentionUserId, input.reason);
+      AddToAttentionSetOp op = opFactory.create(attentionUserId, input.reason, true);
       bu.addOp(changeResource.getId(), op);
+      NotifyHandling notify = input.notify == null ? NotifyHandling.OWNER : input.notify;
+      NotifyResolver.Result notifyResult = notifyResolver.resolve(notify, input.notifyDetails);
+      bu.setNotify(notifyResult);
       bu.execute();
       return Response.ok(accountLoaderFactory.create(true).fillOne(attentionUserId));
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 52a8f47..466ea3c 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -49,6 +49,7 @@
 import com.google.gerrit.server.change.WorkInProgressOp;
 import com.google.gerrit.server.restapi.change.Reviewed.DeleteReviewed;
 import com.google.gerrit.server.restapi.change.Reviewed.PutReviewed;
+import com.google.gerrit.server.util.AttentionSetEmail;
 
 public class Module extends RestApiModule {
   @Override
@@ -217,5 +218,6 @@
     factory(SetTopicOp.Factory.class);
     factory(AddToAttentionSetOp.Factory.class);
     factory(RemoveFromAttentionSetOp.Factory.class);
+    factory(AttentionSetEmail.Factory.class);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
index ae2f2bf..c4dd04e 100644
--- a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
+++ b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -24,6 +25,7 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.AttentionSetEntryResource;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -39,15 +41,18 @@
   private final BatchUpdate.Factory updateFactory;
   private final RemoveFromAttentionSetOp.Factory opFactory;
   private final AccountResolver accountResolver;
+  private final NotifyResolver notifyResolver;
 
   @Inject
   RemoveFromAttentionSet(
       BatchUpdate.Factory updateFactory,
       RemoveFromAttentionSetOp.Factory opFactory,
-      AccountResolver accountResolver) {
+      AccountResolver accountResolver,
+      NotifyResolver notifyResolver) {
     this.updateFactory = updateFactory;
     this.opFactory = opFactory;
     this.accountResolver = accountResolver;
+    this.notifyResolver = notifyResolver;
   }
 
   @Override
@@ -81,8 +86,11 @@
         updateFactory.create(
             changeResource.getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
       RemoveFromAttentionSetOp op =
-          opFactory.create(attentionResource.getAccountId(), input.reason);
+          opFactory.create(attentionResource.getAccountId(), input.reason, true);
       bu.addOp(changeResource.getId(), op);
+      NotifyHandling notify = input.notify == null ? NotifyHandling.OWNER : input.notify;
+      NotifyResolver.Result notifyResult = notifyResolver.resolve(notify, input.notifyDetails);
+      bu.setNotify(notifyResult);
       bu.execute();
     }
     return Response.none();
diff --git a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
index c7b5ad4..6aac835 100644
--- a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
+++ b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
@@ -152,7 +152,7 @@
       Account.Id currentUser) {
     // Replying removes the publishing user from the attention set.
     RemoveFromAttentionSetOp removeFromAttentionSetOp =
-        removeFromAttentionSetOpFactory.create(currentUser, "removed on reply");
+        removeFromAttentionSetOpFactory.create(currentUser, "removed on reply", false);
     bu.addOp(changeNotes.getChangeId(), removeFromAttentionSetOp);
 
     // The rest of the conditions only apply if the change is ready for review
@@ -164,14 +164,14 @@
     if (currentUser.equals(uploader) && !uploader.equals(owner)) {
       // When the uploader replies, add the owner to the attention set.
       AddToAttentionSetOp addToAttentionSetOp =
-          addToAttentionSetOpFactory.create(owner, "uploader replied");
+          addToAttentionSetOpFactory.create(owner, "uploader replied", false);
       bu.addOp(changeNotes.getChangeId(), addToAttentionSetOp);
     }
     if (currentUser.equals(uploader) || currentUser.equals(owner)) {
       // When the owner or uploader replies, add the reviewers to the attention set.
       for (Account.Id reviewer : reviewers) {
         AddToAttentionSetOp addToAttentionSetOp =
-            addToAttentionSetOpFactory.create(reviewer, "owner or uploader replied");
+            addToAttentionSetOpFactory.create(reviewer, "owner or uploader replied", false);
         bu.addOp(changeNotes.getChangeId(), addToAttentionSetOp);
       }
     }
@@ -179,11 +179,12 @@
       // When neither the uploader nor the owner (reviewer or cc) replies, add the owner and the
       // uploader to the attention set.
       AddToAttentionSetOp addToAttentionSetOp =
-          addToAttentionSetOpFactory.create(owner, "reviewer or cc replied");
+          addToAttentionSetOpFactory.create(owner, "reviewer or cc replied", false);
       bu.addOp(changeNotes.getChangeId(), addToAttentionSetOp);
 
       if (owner.get() != uploader.get()) {
-        addToAttentionSetOp = addToAttentionSetOpFactory.create(uploader, "reviewer or cc replied");
+        addToAttentionSetOp =
+            addToAttentionSetOpFactory.create(uploader, "reviewer or cc replied", false);
         bu.addOp(changeNotes.getChangeId(), addToAttentionSetOp);
       }
     }
@@ -226,7 +227,7 @@
         getAccountIdAndValidateUser(changeNotes, add.user, accountsChangedInCommit);
 
     AddToAttentionSetOp addToAttentionSetOp =
-        addToAttentionSetOpFactory.create(attentionUserId, add.reason);
+        addToAttentionSetOpFactory.create(attentionUserId, add.reason, true);
     bu.addOp(changeNotes.getChangeId(), addToAttentionSetOp);
   }
 
@@ -242,7 +243,7 @@
         getAccountIdAndValidateUser(changeNotes, remove.user, accountsChangedInCommit);
 
     RemoveFromAttentionSetOp removeFromAttentionSetOp =
-        removeFromAttentionSetOpFactory.create(attentionUserId, remove.reason);
+        removeFromAttentionSetOpFactory.create(attentionUserId, remove.reason, true);
     bu.addOp(changeNotes.getChangeId(), removeFromAttentionSetOp);
   }
 
diff --git a/java/com/google/gerrit/server/util/AttentionSetEmail.java b/java/com/google/gerrit/server/util/AttentionSetEmail.java
new file mode 100644
index 0000000..56b1dda
--- /dev/null
+++ b/java/com/google/gerrit/server/util/AttentionSetEmail.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2020 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.util;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.SendEmailExecutor;
+import com.google.gerrit.server.mail.send.AddToAttentionSetSender;
+import com.google.gerrit.server.mail.send.AttentionSetSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.mail.send.RemoveFromAttentionSetSender;
+import com.google.gerrit.server.update.Context;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class AttentionSetEmail implements Runnable, RequestContext {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public interface Factory {
+
+    /**
+     * factory for sending an email when adding users to the attention set or removing them from it.
+     *
+     * @param sender sender in charge of sending the email, can be {@link AddToAttentionSetSender}
+     *     or {@link RemoveFromAttentionSetSender}.
+     * @param ctx context for sending the email.
+     * @param change the change that the user was added/removed in.
+     * @param reason reason for adding/removing the user.
+     * @param messageId messageId for tracking the email.
+     * @param attentionUserId the user added/removed.
+     */
+    AttentionSetEmail create(
+        AttentionSetSender sender,
+        Context ctx,
+        Change change,
+        String reason,
+        MessageIdGenerator.MessageId messageId,
+        Account.Id attentionUserId);
+  }
+
+  private ExecutorService sendEmailsExecutor;
+  private AttentionSetSender sender;
+  private Context ctx;
+  private Change change;
+  private String reason;
+
+  private MessageIdGenerator.MessageId messageId;
+  private Account.Id attentionUserId;
+
+  @Inject
+  AttentionSetEmail(
+      @SendEmailExecutor ExecutorService executor,
+      @Assisted AttentionSetSender sender,
+      @Assisted Context ctx,
+      @Assisted Change change,
+      @Assisted String reason,
+      @Assisted MessageIdGenerator.MessageId messageId,
+      @Assisted Account.Id attentionUserId) {
+    this.sendEmailsExecutor = executor;
+    this.sender = sender;
+    this.ctx = ctx;
+    this.change = change;
+    this.reason = reason;
+    this.messageId = messageId;
+    this.attentionUserId = attentionUserId;
+  }
+
+  public void sendAsync() {
+    @SuppressWarnings("unused")
+    Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(this);
+  }
+
+  @Override
+  public void run() {
+    try {
+      AccountState accountState =
+          ctx.getUser().isIdentifiedUser() ? ctx.getUser().asIdentifiedUser().state() : null;
+      if (accountState != null) {
+        sender.setFrom(accountState.account().id());
+      }
+      sender.setNotify(ctx.getNotify(change.getId()));
+      sender.setAttentionSetUser(attentionUserId);
+      sender.setReason(reason);
+      sender.setMessageId(messageId);
+      sender.send();
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "send-email comments";
+  }
+
+  @Override
+  public CurrentUser getUser() {
+    return ctx.getUser();
+  }
+}
diff --git a/java/com/google/gerrit/server/util/AttentionSetUtil.java b/java/com/google/gerrit/server/util/AttentionSetUtil.java
index 8252e8e..62cad3f 100644
--- a/java/com/google/gerrit/server/util/AttentionSetUtil.java
+++ b/java/com/google/gerrit/server/util/AttentionSetUtil.java
@@ -24,6 +24,7 @@
 
 /** Common helpers for dealing with attention set data structures. */
 public class AttentionSetUtil {
+
   /** Returns only updates where the user was added. */
   public static ImmutableSet<AttentionSetUpdate> additionsOnly(
       Collection<AttentionSetUpdate> updates) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index ddebd77..83c1134 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gerrit.testing.FakeEmailSender;
 import com.google.inject.Inject;
 import java.time.Duration;
 import java.time.Instant;
@@ -54,6 +55,7 @@
 
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject protected AccountCreator accountCreator;
+  @Inject private FakeEmailSender email;
 
   /** Simulates a fake clock. Uses second granularity. */
   private static class FakeClock implements LongSupplier {
@@ -89,6 +91,7 @@
   @Test
   public void addUser() throws Exception {
     PushOneCommit.Result r = createChange();
+    requestScopeOperations.setApiUser(user.id());
     int accountId =
         change(r).addToAttentionSet(new AttentionSetInput(user.email(), "first"))._accountId;
     assertThat(accountId).isEqualTo(user.id().get());
@@ -102,6 +105,13 @@
         change(r).addToAttentionSet(new AttentionSetInput(user.email(), "second"))._accountId;
     assertThat(accountId).isEqualTo(user.id().get());
     assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+
+    // Only one email since the second add was ignored.
+    String emailBody = Iterables.getOnlyElement(email.getMessages()).body();
+    assertThat(emailBody)
+        .contains(
+            user.fullName()
+                + " added themselves to the attention set of this change.\n The reason is: first.");
   }
 
   @Test
@@ -133,6 +143,8 @@
   public void removeUser() throws Exception {
     PushOneCommit.Result r = createChange();
     change(r).addToAttentionSet(new AttentionSetInput(user.email(), "added"));
+    requestScopeOperations.setApiUser(user.id());
+
     fakeClock.advance(Duration.ofSeconds(42));
     change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed"));
     AttentionSetUpdate expectedAttentionSetUpdate =
@@ -144,6 +156,13 @@
     fakeClock.advance(Duration.ofSeconds(42));
     change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed again"));
     assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+
+    // Only one email since the second remove was ignored.
+    String emailBody = Iterables.getOnlyElement(email.getMessages()).body();
+    assertThat(emailBody)
+        .contains(
+            user.fullName()
+                + " removed themselves from the attention set of this change.\n The reason is: removed.");
   }
 
   @Test
@@ -460,6 +479,7 @@
   @Test
   public void reviewAddsManuallyAddedUserToAttentionSet() throws Exception {
     PushOneCommit.Result r = createChange();
+    requestScopeOperations.setApiUser(user.id());
     ReviewInput reviewInput = ReviewInput.create().addUserToAttentionSet(user.email(), "reason");
 
     change(r).current().review(reviewInput);
@@ -469,12 +489,21 @@
     assertThat(attentionSet.account()).isEqualTo(user.id());
     assertThat(attentionSet.operation()).isEqualTo(AttentionSetUpdate.Operation.ADD);
     assertThat(attentionSet.reason()).isEqualTo("reason");
+
+    // This is the only email since emails are only sent for manual attention set updates (admin was
+    // also added because user replied, but no email was sent).
+    String emailBodyAddUser = Iterables.getOnlyElement(email.getMessages()).body();
+    assertThat(emailBodyAddUser)
+        .contains(
+            user.fullName()
+                + " added themselves to the attention set of this change.\n The reason is: reason.");
   }
 
   @Test
   public void reviewRemovesManuallyRemovedUserFromAttentionSet() throws Exception {
     PushOneCommit.Result r = createChange();
     change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason"));
+    requestScopeOperations.setApiUser(user.id());
 
     ReviewInput reviewInput =
         ReviewInput.create().removeUserFromAttentionSet(user.email(), "reason");
@@ -485,6 +514,12 @@
     assertThat(attentionSet.account()).isEqualTo(user.id());
     assertThat(attentionSet.operation()).isEqualTo(AttentionSetUpdate.Operation.REMOVE);
     assertThat(attentionSet.reason()).isEqualTo("reason");
+
+    String emailBodyAddUser = Iterables.getOnlyElement(email.getMessages()).body();
+    assertThat(emailBodyAddUser)
+        .contains(
+            user.fullName()
+                + " removed themselves from the attention set of this change.\n The reason is: reason.");
   }
 
   @Test
diff --git a/resources/com/google/gerrit/server/mail/AddToAttentionSet.soy b/resources/com/google/gerrit/server/mail/AddToAttentionSet.soy
new file mode 100644
index 0000000..5ea41b2
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/AddToAttentionSet.soy
@@ -0,0 +1,46 @@
+/**
+ * Copyright (C) 2020 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}
+
+/**
+ * The .AddToAttentionSet template will determine the contents of the email related to a
+ * user being added to the attention set.
+ */
+{template .AddToAttentionSet kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param attentionSetUser: ?}
+  {@param reason: ?}
+  {if $fromName == $attentionSetUser}
+  {$fromName} added themselves to the attention set of this change.
+  {else}
+  {$fromName} requires the attention of {$attentionSetUser} to this change.
+  {/if}
+  {\n} The reason is: {$reason}.
+  {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {if $coverLetter}
+    {\n}
+    {\n}
+    {$coverLetter}
+    {\n}
+  {/if}
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/AddToAttentionSetHtml.soy b/resources/com/google/gerrit/server/mail/AddToAttentionSetHtml.soy
new file mode 100644
index 0000000..bac180a
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/AddToAttentionSetHtml.soy
@@ -0,0 +1,43 @@
+/**
+ * Copyright (C) 2020 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 .AddToAttentionSetHtml}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param attentionSetUser: ?}
+  {@param reason: ?}
+  <p>
+    {if $fromName == $attentionSetUser}
+      {$fromName} added themselves to the attention set of this change.
+    {else}
+      {$fromName} requires the attention of {$attentionSetUser} to this change.
+    {/if}
+    {\n} The reason is: {$reason}.
+  </p>
+
+  {if $email.changeUrl}
+    <p>
+      {call .ViewChangeButton data="all" /}
+    </p>
+  {/if}
+
+  {if $coverLetter}
+    <div style="white-space:pre-wrap">{$coverLetter}</div>
+  {/if}
+{/template}
\ No newline at end of file
diff --git a/resources/com/google/gerrit/server/mail/RemoveFromAttentionSet.soy b/resources/com/google/gerrit/server/mail/RemoveFromAttentionSet.soy
new file mode 100644
index 0000000..f116adb
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/RemoveFromAttentionSet.soy
@@ -0,0 +1,46 @@
+/**
+ * Copyright (C) 2020 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}
+
+/**
+ * The .RemoveFromAttentionSet template will determine the contents of the email related to a
+ * user being added to the attention set.
+ */
+{template .RemoveFromAttentionSet kind="text"}
+  {@param change: ?}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param attentionSetUser: ?}
+  {@param reason: ?}
+  {if $fromName == $attentionSetUser}
+  {$fromName} removed themselves from the attention set of this change.
+  {else}
+  {$fromName} doesn't require the attention of {$attentionSetUser} to this change.
+  {/if}
+  {\n} The reason is: {$reason}.
+  {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {if $coverLetter}
+    {\n}
+    {\n}
+    {$coverLetter}
+    {\n}
+  {/if}
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/RemoveFromAttentionSetHtml.soy b/resources/com/google/gerrit/server/mail/RemoveFromAttentionSetHtml.soy
new file mode 100644
index 0000000..55eef13
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/RemoveFromAttentionSetHtml.soy
@@ -0,0 +1,43 @@
+/**
+ * Copyright (C) 2020 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 .RemoveFromAttentionSetHtml}
+  {@param coverLetter: ?}
+  {@param email: ?}
+  {@param fromName: ?}
+  {@param attentionSetUser: ?}
+  {@param reason: ?}
+  <p>
+    {if $fromName == $attentionSetUser}
+      {$fromName} removed themselves from the attention set of this change.
+    {else}
+      {$fromName} doesn't require the attention of {$attentionSetUser} to this change.
+    {/if}
+    {\n} The reason is: {$reason}.
+  </p>
+
+  {if $email.changeUrl}
+    <p>
+      {call .ViewChangeButton data="all" /}
+    </p>
+  {/if}
+
+  {if $coverLetter}
+    <div style="white-space:pre-wrap">{$coverLetter}</div>
+  {/if}
+{/template}
\ No newline at end of file