Send email notifications to owners on serviceuser owners

Change-Id: I4ce32ef263a882f01d9af6803d90d62d1d35ed3c
diff --git a/BUILD b/BUILD
index 7aaa331..681d904 100644
--- a/BUILD
+++ b/BUILD
@@ -9,6 +9,7 @@
         "Gerrit-Module: com.googlesource.gerrit.plugins.serviceuser.Module",
         "Gerrit-HttpModule: com.googlesource.gerrit.plugins.serviceuser.HttpModule",
         "Gerrit-SshModule: com.googlesource.gerrit.plugins.serviceuser.SshModule",
+        "Gerrit-InitStep: com.googlesource.gerrit.plugins.serviceuser.Init",
     ],
     resource_jars = ["//plugins/serviceuser/web:serviceuser"],
     resources = glob(["src/main/resources/**/*"]),
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/AddSshKey.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/AddSshKey.java
index c5ccadd..ed5c4ba 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/AddSshKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/AddSshKey.java
@@ -23,21 +23,31 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 class AddSshKey implements RestModifyView<ServiceUserResource, SshKeyInput> {
   private final Provider<com.google.gerrit.server.restapi.account.AddSshKey> addSshKey;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
-  AddSshKey(Provider<com.google.gerrit.server.restapi.account.AddSshKey> addSshKey) {
+  AddSshKey(
+      Provider<com.google.gerrit.server.restapi.account.AddSshKey> addSshKey,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.addSshKey = addSshKey;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
   public Response<SshKeyInfo> apply(ServiceUserResource rsrc, SshKeyInput input)
       throws AuthException, BadRequestException, IOException, ConfigInvalidException {
-    return addSshKey.get().apply(rsrc.getUser(), input);
+    Response<SshKeyInfo> resp = addSshKey.get().apply(rsrc.getUser(), input);
+    if (resp.statusCode() == Response.created().statusCode()) {
+      outgoingEmailFactory.create(rsrc, Operation.ADD_SSH_KEY).send();
+    }
+    return resp;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateToken.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateToken.java
index 199f2bf..449657a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateToken.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateToken.java
@@ -31,6 +31,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -42,6 +44,7 @@
   private final com.google.gerrit.server.restapi.account.CreateToken createToken;
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
   CreateToken(
@@ -49,11 +52,13 @@
       PluginConfigFactory pluginConfigFactory,
       com.google.gerrit.server.restapi.account.CreateToken createToken,
       Provider<CurrentUser> self,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.config = pluginConfigFactory.getFromGerritConfig(pluginName);
     this.createToken = createToken;
     this.self = self;
     this.permissionBackend = permissionBackend;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
@@ -72,6 +77,11 @@
       }
     }
 
-    return createToken.apply(rsrc.getUser(), id.get(), input);
+    Response<AuthTokenInfo> resp =
+        createToken.apply(rsrc.getUser(), id.get(), input);
+    if (resp.statusCode() == Response.created().statusCode()) {
+      outgoingEmailFactory.create(rsrc, Operation.CREATE_TOKEN).send();
+    }
+    return resp;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteActive.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteActive.java
index ec5d063..e90cece 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteActive.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteActive.java
@@ -21,21 +21,32 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 class DeleteActive implements RestModifyView<ServiceUserResource, Input> {
   private final Provider<com.google.gerrit.server.restapi.account.DeleteActive> deleteActive;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
-  DeleteActive(Provider<com.google.gerrit.server.restapi.account.DeleteActive> deleteActive) {
+  DeleteActive(
+      Provider<com.google.gerrit.server.restapi.account.DeleteActive> deleteActive,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.deleteActive = deleteActive;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
   public Response<?> apply(ServiceUserResource rsrc, Input input)
       throws RestApiException, IOException, ConfigInvalidException {
-    return deleteActive.get().apply(rsrc, input);
+    Response<?> resp = deleteActive.get().apply(rsrc, input);
+
+    if (resp.statusCode() == Response.none().statusCode()) {
+      outgoingEmailFactory.create(rsrc, Operation.INACTIVATE).send();
+    }
+    return resp;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java
index 691f33f..338cae4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java
@@ -22,6 +22,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -29,10 +31,14 @@
 @Singleton
 class DeleteSshKey implements RestModifyView<ServiceUserResource.SshKey, Input> {
   private final Provider<com.google.gerrit.server.restapi.account.DeleteSshKey> deleteSshKey;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
-  DeleteSshKey(Provider<com.google.gerrit.server.restapi.account.DeleteSshKey> deleteSshKey) {
+  DeleteSshKey(
+      Provider<com.google.gerrit.server.restapi.account.DeleteSshKey> deleteSshKey,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.deleteSshKey = deleteSshKey;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
@@ -42,6 +48,11 @@
           IOException,
           ConfigInvalidException,
           PermissionBackendException {
-    return deleteSshKey.get().apply(rsrc.getUser(), rsrc.getSshKey());
+    Response<?> resp = deleteSshKey.get().apply(rsrc.getUser(), rsrc.getSshKey());
+
+    if (resp.statusCode() == Response.none().statusCode()) {
+      outgoingEmailFactory.create(rsrc, Operation.DELETE_SSH_KEY).send();
+    }
+    return resp;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteToken.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteToken.java
index bc38f48..8662fa3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteToken.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteToken.java
@@ -29,6 +29,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -38,6 +40,7 @@
   private final com.google.gerrit.server.restapi.account.DeleteToken deleteToken;
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
   DeleteToken(
@@ -45,11 +48,13 @@
       PluginConfigFactory pluginConfigFactory,
       com.google.gerrit.server.restapi.account.DeleteToken deleteToken,
       Provider<CurrentUser> self,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.config = pluginConfigFactory.getFromGerritConfig(pluginName);
     this.deleteToken = deleteToken;
     this.self = self;
     this.permissionBackend = permissionBackend;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
@@ -64,6 +69,10 @@
       permissionBackend.user(self.get()).check(ADMINISTRATE_SERVER);
     }
 
-    return deleteToken.apply(rsrc.getUser(), rsrc.getToken().id(), false);
+    Response<String> resp = deleteToken.apply(rsrc.getUser(), rsrc.getToken().id(), false);
+    if (resp.statusCode() == Response.none().statusCode()) {
+      outgoingEmailFactory.create(rsrc, Operation.DELETE_TOKEN).send();
+    }
+    return resp;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
index f0b92dd..d71adf6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
@@ -37,7 +37,7 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-class GetServiceUser implements RestReadView<ServiceUserResource> {
+public class GetServiceUser implements RestReadView<ServiceUserResource> {
   private final Provider<GetAccount> getAccount;
   private final GetOwner getOwner;
   private final AccountLoader.Factory accountLoader;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Init.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Init.java
new file mode 100644
index 0000000..e5027fe
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Init.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2025 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.googlesource.gerrit.plugins.serviceuser;
+
+import static com.google.gerrit.common.FileUtil.chmod;
+import static com.google.gerrit.pgm.init.api.InitUtil.extract;
+
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserEmailModule;
+import java.nio.file.Path;
+
+public class Init implements InitStep {
+  private final SitePaths site;
+
+  @Inject
+  Init(SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  public void run() throws Exception {
+    extractMail("ServiceUserUpdated.soy");
+    extractMail("ServiceUserUpdatedHtml.soy");
+  }
+
+  private void extractMail(String orig) throws Exception {
+    Path ex = site.mail_dir.resolve(orig);
+    extract(ex, ServiceUserEmailModule.class, orig);
+    chmod(0444, ex);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
index fbfc1c0..6e189fd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
@@ -33,6 +33,7 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserEmailModule;
 
 class Module extends AbstractModule {
 
@@ -82,6 +83,7 @@
         });
     install(new HttpModule());
     install(StorageCache.module());
+    install(new ServiceUserEmailModule());
   }
 
   @Provides
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutActive.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutActive.java
index f4b0365..6b3b332 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutActive.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutActive.java
@@ -21,21 +21,33 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 class PutActive implements RestModifyView<ServiceUserResource, Input> {
   private final Provider<com.google.gerrit.server.restapi.account.PutActive> putActive;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
-  PutActive(Provider<com.google.gerrit.server.restapi.account.PutActive> putActive) {
+  PutActive(
+      Provider<com.google.gerrit.server.restapi.account.PutActive> putActive,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.putActive = putActive;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
   public Response<String> apply(ServiceUserResource rsrc, Input input)
       throws IOException, ConfigInvalidException, RestApiException {
-    return putActive.get().apply(rsrc, input);
+
+    Response<String> resp = putActive.get().apply(rsrc, input);
+    if (resp.statusCode() == Response.created().statusCode()) {
+      outgoingEmailFactory.create(rsrc, Operation.ACTIVATE).send();
+    }
+
+    return resp;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java
index cb66be7..49ec008 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java
@@ -36,6 +36,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.serviceuser.PutEmail.Input;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -52,6 +54,7 @@
   private final Provider<PutPreferred> putPreferred;
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
   PutEmail(
@@ -61,7 +64,8 @@
       Provider<DeleteEmail> deleteEmail,
       Provider<PutPreferred> putPreferred,
       Provider<CurrentUser> self,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.getConfig = getConfig;
     this.getEmail = getEmail;
     this.createEmail = createEmail;
@@ -69,6 +73,7 @@
     this.putPreferred = putPreferred;
     this.self = self;
     this.permissionBackend = permissionBackend;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
@@ -95,11 +100,16 @@
       throw asRestApiException("Cannot get email", e);
     }
 
+    Response<?> resp;
     if (Strings.emptyToNull(input.email) == null) {
       if (Strings.emptyToNull(email) == null) {
         return Response.none();
       }
-      return deleteEmail.get().apply(rsrc.getUser(), email);
+      resp = deleteEmail.get().apply(rsrc.getUser(), email);
+      if (resp.statusCode() == Response.none().statusCode()) {
+        outgoingEmailFactory.create(rsrc, Operation.DELETE_EMAIL).send();
+      }
+      return resp;
     } else if (email != null && email.equals(input.email)) {
       return Response.ok(email);
     } else {
@@ -111,6 +121,9 @@
       in.noConfirmation = true;
       createEmail.get().apply(rsrc.getUser(), IdString.fromDecoded(in.email), in);
       putPreferred.get().apply(rsrc.getUser(), input.email);
+
+      outgoingEmailFactory.create(rsrc, Operation.ADD_EMAIL).send();
+
       return Response.ok(input.email);
     }
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutName.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutName.java
index 2906daa..63bb8d0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutName.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutName.java
@@ -22,16 +22,22 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
 class PutName implements RestModifyView<ServiceUserResource, NameInput> {
   private Provider<com.google.gerrit.server.restapi.account.PutName> putName;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
-  PutName(Provider<com.google.gerrit.server.restapi.account.PutName> putName) {
+  PutName(
+      Provider<com.google.gerrit.server.restapi.account.PutName> putName,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.putName = putName;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
@@ -40,6 +46,12 @@
           ResourceNotFoundException,
           IOException,
           ConfigInvalidException {
-    return putName.get().apply(rsrc.getUser(), input);
+    Response<String> resp = putName.get().apply(rsrc.getUser(), input);
+    if (resp.statusCode() == Response.none().statusCode()) {
+      outgoingEmailFactory.create(rsrc, Operation.DELETE_NAME).send();
+    } else if (resp.statusCode() == Response.ok().statusCode()) {
+      outgoingEmailFactory.create(rsrc, Operation.UPDATE_NAME).send();
+    }
+    return resp;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
index e3af6a0..0c57bb9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
@@ -45,6 +45,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.serviceuser.PutOwner.Input;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserOutgoingEmail;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
@@ -64,6 +66,7 @@
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
   private final StorageCache storageCache;
+  private final ServiceUserOutgoingEmail.Factory outgoingEmailFactory;
 
   @Inject
   PutOwner(
@@ -75,7 +78,8 @@
       GroupJson json,
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
-      StorageCache storageCache) {
+      StorageCache storageCache,
+      ServiceUserOutgoingEmail.Factory outgoingEmailFactory) {
     this.getConfig = getConfig;
     this.groups = groups;
     this.configProvider = configProvider;
@@ -85,6 +89,7 @@
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.storageCache = storageCache;
+    this.outgoingEmailFactory = outgoingEmailFactory;
   }
 
   @Override
@@ -130,10 +135,18 @@
       throw asRestApiException("Invalid configuration", e);
     }
 
-    return group != null
-        ? (oldGroup != null
-            ? Response.ok(json.format(group))
-            : Response.created(json.format(group)))
-        : Response.<GroupInfo>none();
+    Response<GroupInfo> resp;
+    if (group != null) {
+      if (oldGroup != null) {
+        resp = Response.ok(json.format(group));
+      } else {
+        resp = Response.created(json.format(group));
+      }
+      outgoingEmailFactory.create(rsrc, Operation.UPDATE_OWNER).send();
+    } else {
+      resp = Response.none();
+      outgoingEmailFactory.create(rsrc, Operation.DELETE_OWNER).send();
+    }
+    return resp;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserResource.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserResource.java
index 5e43f35..48ab903 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserResource.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserResource.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.server.account.AuthToken;
 import com.google.inject.TypeLiteral;
 
-class ServiceUserResource extends AccountResource {
+public class ServiceUserResource extends AccountResource {
   static final TypeLiteral<RestView<ServiceUserResource>> SERVICE_USER_KIND =
       new TypeLiteral<RestView<ServiceUserResource>>() {};
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserEmailModule.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserEmailModule.java
new file mode 100644
index 0000000..072dbff
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserEmailModule.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2025 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.googlesource.gerrit.plugins.serviceuser.email;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
+
+public class ServiceUserEmailModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), MailSoyTemplateProvider.class)
+        .to(ServiceUserMailSoyTemplateProvider.class)
+        .in(SINGLETON);
+    factory(ServiceUserUpdatedEmailDecorator.Factory.class);
+    factory(ServiceUserOutgoingEmail.Factory.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserMailSoyTemplateProvider.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserMailSoyTemplateProvider.java
new file mode 100644
index 0000000..63ddf13
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserMailSoyTemplateProvider.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2025 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.googlesource.gerrit.plugins.serviceuser.email;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
+import java.util.Set;
+
+public class ServiceUserMailSoyTemplateProvider implements MailSoyTemplateProvider {
+
+  @Override
+  public String getPath() {
+    return "com/googlesource/gerrit/plugins/serviceuser/email/";
+  }
+
+  @Override
+  public Set<String> getFileNames() {
+    return ImmutableSet.of("ServiceUserUpdated.soy", "ServiceUserUpdatedHtml.soy");
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserOutgoingEmail.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserOutgoingEmail.java
new file mode 100644
index 0000000..9b55e6c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserOutgoingEmail.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2025 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.googlesource.gerrit.plugins.serviceuser.email;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.server.mail.send.OutgoingEmailFactory;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import com.googlesource.gerrit.plugins.serviceuser.ServiceUserResource;
+import com.googlesource.gerrit.plugins.serviceuser.email.ServiceUserUpdatedEmailDecorator.Operation;
+
+public class ServiceUserOutgoingEmail {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final ServiceUserUpdatedEmailDecorator.Factory emailDecoratorFactory;
+  private final OutgoingEmailFactory outgoingEmailFactory;
+
+  private final ServiceUserResource serviceUserResource;
+  private final Operation operation;
+
+  public interface Factory {
+    ServiceUserOutgoingEmail create(ServiceUserResource serviceUserResource, Operation operation);
+  }
+
+  @AssistedInject
+  public ServiceUserOutgoingEmail(
+      ServiceUserUpdatedEmailDecorator.Factory emailDecoratorFactory,
+      OutgoingEmailFactory outgoingEmailFactory,
+      @Assisted ServiceUserResource serviceUserResource,
+      @Assisted Operation operation) {
+    this.emailDecoratorFactory = emailDecoratorFactory;
+    this.outgoingEmailFactory = outgoingEmailFactory;
+
+    this.serviceUserResource = serviceUserResource;
+    this.operation = operation;
+  }
+
+  public void send() {
+    try {
+      outgoingEmailFactory
+          .create(
+              "ServiceUserUpdated", emailDecoratorFactory.create(serviceUserResource, operation))
+          .send();
+    } catch (EmailException e) {
+      logger.atSevere().withCause(e).log("Failed to send email about serviceuser update");
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdatedEmailDecorator.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdatedEmailDecorator.java
new file mode 100644
index 0000000..8d00065
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdatedEmailDecorator.java
@@ -0,0 +1,168 @@
+// Copyright (C) 2025 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.googlesource.gerrit.plugins.serviceuser.email;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.DefaultUrlFormatter;
+import com.google.gerrit.server.group.GroupResolver;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.mail.send.OutgoingEmail;
+import com.google.gerrit.server.mail.send.OutgoingEmail.EmailDecorator;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.group.ListMembers;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import com.googlesource.gerrit.plugins.serviceuser.GetServiceUser;
+import com.googlesource.gerrit.plugins.serviceuser.GetServiceUser.ServiceUserInfo;
+import com.googlesource.gerrit.plugins.serviceuser.ServiceUserResource;
+import java.io.IOException;
+
+public class ServiceUserUpdatedEmailDecorator implements EmailDecorator {
+  public enum Operation {
+    ADD_SSH_KEY("An SSH key was added."),
+    CREATE_TOKEN("An authentication token was created."),
+    INACTIVATE("The service user was inactivated."),
+    DELETE_SSH_KEY("An SSH key was deleted."),
+    DELETE_TOKEN("An authentication token was deleted."),
+    ACTIVATE("The service user was activated."),
+    DELETE_EMAIL("The email address was deleted."),
+    ADD_EMAIL("The email address was updated."),
+    DELETE_NAME("The full name was deleted."),
+    UPDATE_NAME("The full name was updated."),
+    UPDATE_OWNER("The owner group was updated."),
+    DELETE_OWNER("The owner group was deleted.");
+
+    public final String description;
+
+    Operation(String description) {
+      this.description = description;
+    }
+  }
+
+  public interface Factory {
+    ServiceUserUpdatedEmailDecorator create(
+        ServiceUserResource serviceUserResource, Operation operation);
+  }
+
+  private OutgoingEmail email;
+
+  private final Provider<GetServiceUser> getServiceUser;
+  private final Provider<ListMembers> listMembers;
+  private final OneOffRequestContext oneOffRequestContext;
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupResolver groupResolver;
+  private final MessageIdGenerator messageIdGenerator;
+  private final DefaultUrlFormatter urlFomatter;
+
+  private final ServiceUserResource serviceUserResource;
+  private final Operation operation;
+
+  private ServiceUserInfo serviceUserInfo;
+
+  @AssistedInject
+  public ServiceUserUpdatedEmailDecorator(
+      Provider<GetServiceUser> getServiceUser,
+      Provider<ListMembers> listMembers,
+      OneOffRequestContext oneOffRequestContext,
+      GroupControl.Factory groupControlFactory,
+      GroupResolver groupResolver,
+      MessageIdGenerator messageIdGenerator,
+      DefaultUrlFormatter urlFomatter,
+      @Assisted ServiceUserResource serviceUserResource,
+      @Assisted Operation operation) {
+    this.getServiceUser = getServiceUser;
+    this.listMembers = listMembers;
+    this.oneOffRequestContext = oneOffRequestContext;
+    this.groupControlFactory = groupControlFactory;
+    this.groupResolver = groupResolver;
+    this.messageIdGenerator = messageIdGenerator;
+    this.urlFomatter = urlFomatter;
+
+    this.serviceUserResource = serviceUserResource;
+    this.operation = operation;
+  }
+
+  @Override
+  public void init(OutgoingEmail email) throws EmailException {
+    this.email = email;
+
+    try (ManualRequestContext ctx = oneOffRequestContext.open()) {
+      try {
+        serviceUserInfo = getServiceUser.get().apply(serviceUserResource).value();
+      } catch (IOException | RestApiException | PermissionBackendException e) {
+        throw new EmailException(
+            String.format(
+                "Failed to get service user details for %s",
+                serviceUserResource.getUser().getUserName()),
+            e);
+      }
+    }
+
+    this.email.setHeader(
+        "Subject",
+        String.format(
+            "[Gerrit Code Review] Service User '%s' has been updated.", serviceUserInfo.username));
+    this.email.setMessageId(
+        messageIdGenerator.fromReasonAccountIdAndTimestamp(
+            "Serviceuser_updated", Account.id(serviceUserInfo._accountId), TimeUtil.now()));
+
+    if (serviceUserInfo.owner == null) {
+      this.email.addByAccountId(RecipientType.TO, Account.id(serviceUserInfo.createdBy._accountId));
+    } else {
+      try (ManualRequestContext ctx = oneOffRequestContext.open()) {
+        GroupDescription.Basic group = groupResolver.parseId(serviceUserInfo.owner.id);
+        GroupControl ctl = groupControlFactory.controlFor(group);
+        ListMembers lm = listMembers.get();
+        GroupResource rsrc = new GroupResource(ctl);
+        lm.setRecursive(true);
+        try {
+          for (AccountInfo a : lm.apply(rsrc).value()) {
+            this.email.addByAccountId(RecipientType.TO, Account.id(a._accountId));
+          }
+        } catch (Exception e) {
+          throw new EmailException(
+              "Could not compute receipients for serviceuser update notice.", e);
+        }
+      }
+    }
+  }
+
+  @Override
+  public void populateEmailContent() throws EmailException {
+    email.addSoyEmailDataParam("serviceUserName", serviceUserInfo.username);
+    email.addSoyEmailDataParam("operation", operation.description);
+    email.addSoyEmailDataParam(
+        "serviceUserUrl",
+        urlFomatter
+            .getRestUrl(String.format("x/serviceuser/user/%d", serviceUserInfo._accountId))
+            .get());
+
+    email.appendText(email.textTemplate("ServiceUserUpdated"));
+    if (email.useHtml()) {
+      email.appendHtml(email.soyHtmlTemplate("ServiceUserUpdatedHtml"));
+    }
+  }
+}
diff --git a/src/main/resources/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdated.soy b/src/main/resources/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdated.soy
new file mode 100644
index 0000000..a6ff0e9
--- /dev/null
+++ b/src/main/resources/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdated.soy
@@ -0,0 +1,59 @@
+/**
+ * Copyright (C) 2025 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.ServiceUserUpdated}
+
+/**
+ * The .AuthTokenUpdate template will determine the contents of the email related to
+ * adding, changing or deleting the token.
+ */
+{template ServiceUserUpdated kind="text"}
+  {@param email: ?}
+  The serviceuser with username "{$email.serviceUserName}" was updated{sp}
+  on Gerrit Code Review at {sp}{$email.gerritHost}. The following change was made:
+
+  {\n}
+  {\n}
+  {$email.operation}
+  {\n}
+  {\n}
+
+  If this is not expected, please contact your Gerrit Administrators
+  immediately.
+
+  {\n}
+  {\n}
+
+  You can manage this serviceuser by visiting
+  {\n}
+  {$email.serviceUserUrl}.
+  {\n}
+
+  {\n}
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a new
+  browser window instead.
+
+  {\n}
+  {\n}
+
+  This is a send-only email address. Replies to this message will not be read
+  or answered.
+
+  {\n}
+  {\n}
+{/template}
diff --git a/src/main/resources/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdatedHtml.soy b/src/main/resources/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdatedHtml.soy
new file mode 100644
index 0000000..da042c1
--- /dev/null
+++ b/src/main/resources/com/googlesource/gerrit/plugins/serviceuser/email/ServiceUserUpdatedHtml.soy
@@ -0,0 +1,47 @@
+/**
+ * Copyright (C) 2025 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.ServiceUserUpdatedHtml}
+
+{template ServiceUserUpdatedHtml}
+  {@param email: ?}
+  <p>
+    The serviceuser with username "{$email.serviceUserName}" was updated{sp}
+  	on Gerrit Code Review at {sp}{$email.gerritHost}. The following change was made:
+  </p>
+
+  <p><i>{$email.operation}</i></p>
+
+  <p>
+     If this is not expected, please contact your Gerrit Administrators
+  	immediately.
+  </p>
+
+  <p>
+    You can manage this serviceuser by visiting {sp}
+    <a href="{$email.serviceUserUrl}">{$email.serviceUserUrl}</a>.
+  </p>
+
+  <p>
+    If clicking the link above does not work, copy and paste the URL in a new
+    browser window instead.
+  </p>
+
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}