Merge "Fix chat panel visibility issues in dark theme"
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index b3e4dfc..316fee9 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -92,6 +92,7 @@
 import com.google.gerrit.server.config.GerritRuntime;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.LogConfig;
+import com.google.gerrit.server.config.SendEmailEnabledModule;
 import com.google.gerrit.server.config.SysExecutorModule;
 import com.google.gerrit.server.events.EventBroker.EventBrokerModule;
 import com.google.gerrit.server.events.StreamEventsApiListener.StreamEventsApiListenerModule;
@@ -543,6 +544,7 @@
     modules.add(new StartupChecksModule());
     modules.add(new GerritInstanceNameModule());
     modules.add(new GerritInstanceIdModule());
+    modules.add(new SendEmailEnabledModule());
     if (MoreObjects.firstNonNull(httpd, true)) {
       modules.add(
           new CanonicalWebUrlModule() {
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 3f7b2bb..fad9208 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -58,6 +58,7 @@
 import com.google.gerrit.server.change.ReviewerModifier.InternalReviewerInput;
 import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
 import com.google.gerrit.server.change.ReviewerModifier.ReviewerModificationList;
+import com.google.gerrit.server.config.SendEmailEnabled;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.CommentAdded;
@@ -136,6 +137,7 @@
   private final DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory;
   private final PluginSetContext<ValidationOptionsListener> validationOptionsListeners;
   private final PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners;
+  private final boolean sendEmailEnabled;
 
   private final Change.Id changeId;
   private final PatchSet.Id psId;
@@ -184,6 +186,7 @@
       ChangeMessagesUtil cmUtil,
       EmailFactories emailFactories,
       @SendEmailExecutor ExecutorService sendEmailExecutor,
+      @SendEmailEnabled Boolean sendEmailEnabled,
       CommitValidators.Factory commitValidatorsFactory,
       TopicValidator topicValidator,
       CommentAdded commentAdded,
@@ -226,6 +229,7 @@
     this.approvals = Collections.emptyMap();
     this.fireRevisionCreated = true;
     this.sendMail = true;
+    this.sendEmailEnabled = sendEmailEnabled;
     this.updateRef = true;
   }
 
@@ -601,7 +605,7 @@
   public void postUpdate(PostUpdateContext ctx) throws Exception {
     reviewerAdditions.postUpdate(ctx);
     NotifyResolver.Result notify = ctx.getNotify(change.getId());
-    if (sendMail) {
+    if (sendMail && sendEmailEnabled) {
       Runnable sender =
           new Runnable() {
             @Override
diff --git a/java/com/google/gerrit/server/change/EmailNewPatchSet.java b/java/com/google/gerrit/server/change/EmailNewPatchSet.java
index 30d82a4..dc6935e 100644
--- a/java/com/google/gerrit/server/change/EmailNewPatchSet.java
+++ b/java/com/google/gerrit/server/change/EmailNewPatchSet.java
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.server.mail.EmailFactories.NEW_PATCHSET_ADDED;
 
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -29,6 +31,7 @@
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.SendEmailEnabled;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.EmailFactories;
 import com.google.gerrit.server.mail.send.ChangeEmail;
@@ -68,13 +71,15 @@
 
   private final ExecutorService sendEmailExecutor;
   private final ThreadLocalRequestContext threadLocalRequestContext;
-  private final AsyncSender asyncSender;
+  private final Supplier<AsyncSender> asyncSenderSupplier;
+  private final boolean sendEmailEnabled;
 
   private RequestScopePropagator requestScopePropagator;
 
   @Inject
   EmailNewPatchSet(
       @SendEmailExecutor ExecutorService sendEmailExecutor,
+      @SendEmailEnabled Boolean sendEmailEnabled,
       ThreadLocalRequestContext threadLocalRequestContext,
       EmailFactories emailFactories,
       PatchSetInfoFactory patchSetInfoFactory,
@@ -88,45 +93,49 @@
       @Assisted ChangeKind changeKind,
       @Assisted ObjectId preUpdateMetaId) {
     this.sendEmailExecutor = sendEmailExecutor;
+    this.sendEmailEnabled = sendEmailEnabled;
     this.threadLocalRequestContext = threadLocalRequestContext;
 
-    MessageId messageId;
-    try {
-      messageId =
-          messageIdGenerator.fromChangeUpdateAndReason(
-              postUpdateContext.getRepoView(), patchSet.id(), "EmailReplacePatchSet");
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
+    this.asyncSenderSupplier =
+        Suppliers.memoize(
+            () -> {
+              MessageId messageId;
+              try {
+                messageId =
+                    messageIdGenerator.fromChangeUpdateAndReason(
+                        postUpdateContext.getRepoView(), patchSet.id(), "EmailReplacePatchSet");
+              } catch (IOException e) {
+                throw new UncheckedIOException(e);
+              }
 
-    Change.Id changeId = patchSet.id().changeId();
+              Change.Id changeId = patchSet.id().changeId();
 
-    // Getting the change data from PostUpdateContext retrieves a cached ChangeData
-    // instance. This ChangeData instance has been created when the change was (re)indexed
-    // due to the update, and hence has submit requirement results already cached (since
-    // (re)indexing triggers the evaluation of the submit requirements).
-    Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults =
-        postUpdateContext
-            .getChangeData(postUpdateContext.getProject(), changeId)
-            .submitRequirementsIncludingLegacy();
-    this.asyncSender =
-        new AsyncSender(
-            postUpdateContext.getIdentifiedUser(),
-            emailFactories,
-            patchSetInfoFactory,
-            messageId,
-            postUpdateContext.getNotify(changeId),
-            postUpdateContext.getProject(),
-            changeId,
-            patchSet,
-            message,
-            postUpdateContext.getWhen(),
-            outdatedApprovals,
-            reviewers,
-            extraCcs,
-            changeKind,
-            preUpdateMetaId,
-            postUpdateSubmitRequirementResults);
+              // Getting the change data from PostUpdateContext retrieves a cached ChangeData
+              // instance. This ChangeData instance has been created when the change was (re)indexed
+              // due to the update, and hence has submit requirement results already cached (since
+              // (re)indexing triggers the evaluation of the submit requirements).
+              Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults =
+                  postUpdateContext
+                      .getChangeData(postUpdateContext.getProject(), changeId)
+                      .submitRequirementsIncludingLegacy();
+              return new AsyncSender(
+                  postUpdateContext.getIdentifiedUser(),
+                  emailFactories,
+                  patchSetInfoFactory,
+                  messageId,
+                  postUpdateContext.getNotify(changeId),
+                  postUpdateContext.getProject(),
+                  changeId,
+                  patchSet,
+                  message,
+                  postUpdateContext.getWhen(),
+                  outdatedApprovals,
+                  reviewers,
+                  extraCcs,
+                  changeKind,
+                  preUpdateMetaId,
+                  postUpdateSubmitRequirementResults);
+            });
   }
 
   public EmailNewPatchSet setRequestScopePropagator(RequestScopePropagator requestScopePropagator) {
@@ -135,15 +144,19 @@
   }
 
   public void sendAsync() {
+    if (!sendEmailEnabled) {
+      return;
+    }
     @SuppressWarnings("unused")
     Future<?> possiblyIgnoredError =
         sendEmailExecutor.submit(
             requestScopePropagator != null
-                ? requestScopePropagator.wrap(asyncSender)
+                ? requestScopePropagator.wrap(asyncSenderSupplier.get())
                 : () -> {
-                  RequestContext old = threadLocalRequestContext.setContext(asyncSender);
+                  RequestContext old =
+                      threadLocalRequestContext.setContext(asyncSenderSupplier.get());
                   try {
-                    asyncSender.run();
+                    asyncSenderSupplier.get().run();
                   } finally {
                     @SuppressWarnings("unused")
                     var unused = threadLocalRequestContext.setContext(old);
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index bb93cd3..880cca3 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -17,6 +17,8 @@
 import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
 import static com.google.gerrit.server.mail.EmailFactories.COMMENTS_ADDED;
 
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -28,6 +30,7 @@
 import com.google.gerrit.entities.SubmitRequirementResult;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.SendEmailEnabled;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.EmailFactories;
 import com.google.gerrit.server.mail.send.ChangeEmail;
@@ -82,11 +85,13 @@
   }
 
   private final ExecutorService sendEmailsExecutor;
-  private final AsyncSender asyncSender;
+  private final Supplier<AsyncSender> asyncSenderSupplier;
+  private final boolean sendEmailEnabled;
 
   @Inject
   EmailReviewComments(
       @SendEmailExecutor ExecutorService executor,
+      @SendEmailEnabled Boolean sendEmailEnabled,
       PatchSetInfoFactory patchSetInfoFactory,
       EmailFactories emailFactories,
       ThreadLocalRequestContext requestContext,
@@ -99,49 +104,56 @@
       @Nullable @Assisted("patchSetComment") String patchSetComment,
       @Assisted List<LabelVote> labels) {
     this.sendEmailsExecutor = executor;
+    this.sendEmailEnabled = sendEmailEnabled;
 
-    MessageId messageId;
-    try {
-      messageId =
-          messageIdGenerator.fromChangeUpdateAndReason(
-              postUpdateContext.getRepoView(), patchSet.id(), "EmailReviewComments");
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
+    this.asyncSenderSupplier =
+        Suppliers.memoize(
+            () -> {
+              MessageId messageId;
+              try {
+                messageId =
+                    messageIdGenerator.fromChangeUpdateAndReason(
+                        postUpdateContext.getRepoView(), patchSet.id(), "EmailReviewComments");
+              } catch (IOException e) {
+                throw new UncheckedIOException(e);
+              }
 
-    Change.Id changeId = patchSet.id().changeId();
+              Change.Id changeId = patchSet.id().changeId();
 
-    // Getting the change data from PostUpdateContext retrieves a cached ChangeData
-    // instance. This ChangeData instance has been created when the change was (re)indexed
-    // due to the update, and hence has submit requirement results already cached (since
-    // (re)indexing triggers the evaluation of the submit requirements).
-    Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults =
-        postUpdateContext
-            .getChangeData(postUpdateContext.getProject(), changeId)
-            .submitRequirementsIncludingLegacy();
-    this.asyncSender =
-        new AsyncSender(
-            requestContext,
-            emailFactories,
-            patchSetInfoFactory,
-            postUpdateContext.getUser().asIdentifiedUser(),
-            messageId,
-            postUpdateContext.getNotify(changeId),
-            postUpdateContext.getProject(),
-            changeId,
-            patchSet,
-            preUpdateMetaId,
-            message,
-            postUpdateContext.getWhen(),
-            ImmutableList.copyOf(COMMENT_ORDER.sortedCopy(comments)),
-            patchSetComment,
-            ImmutableList.copyOf(labels),
-            postUpdateSubmitRequirementResults);
+              // Getting the change data from PostUpdateContext retrieves a cached ChangeData
+              // instance. This ChangeData instance has been created when the change was (re)indexed
+              // due to the update, and hence has submit requirement results already cached (since
+              // (re)indexing triggers the evaluation of the submit requirements).
+              Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults =
+                  postUpdateContext
+                      .getChangeData(postUpdateContext.getProject(), changeId)
+                      .submitRequirementsIncludingLegacy();
+              return new AsyncSender(
+                  requestContext,
+                  emailFactories,
+                  patchSetInfoFactory,
+                  postUpdateContext.getUser().asIdentifiedUser(),
+                  messageId,
+                  postUpdateContext.getNotify(changeId),
+                  postUpdateContext.getProject(),
+                  changeId,
+                  patchSet,
+                  preUpdateMetaId,
+                  message,
+                  postUpdateContext.getWhen(),
+                  ImmutableList.copyOf(COMMENT_ORDER.sortedCopy(comments)),
+                  patchSetComment,
+                  ImmutableList.copyOf(labels),
+                  postUpdateSubmitRequirementResults);
+            });
   }
 
   public void sendAsync() {
+    if (!sendEmailEnabled) {
+      return;
+    }
     @SuppressWarnings("unused")
-    Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(asyncSender);
+    Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(asyncSenderSupplier.get());
   }
 
   /**
diff --git a/java/com/google/gerrit/server/change/ModifyReviewersEmail.java b/java/com/google/gerrit/server/change/ModifyReviewersEmail.java
index cac40d1..369d080 100644
--- a/java/com/google/gerrit/server/change/ModifyReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/ModifyReviewersEmail.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.SendEmailEnabled;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.EmailFactories;
 import com.google.gerrit.server.mail.send.ChangeEmail;
@@ -42,14 +43,17 @@
   private final EmailFactories emailFactories;
   private final ExecutorService sendEmailsExecutor;
   private final MessageIdGenerator messageIdGenerator;
+  private final boolean sendEmailEnabled;
 
   @Inject
   ModifyReviewersEmail(
       EmailFactories emailFactories,
       @SendEmailExecutor ExecutorService sendEmailsExecutor,
+      @SendEmailEnabled Boolean sendEmailEnabled,
       MessageIdGenerator messageIdGenerator) {
     this.emailFactories = emailFactories;
     this.sendEmailsExecutor = sendEmailsExecutor;
+    this.sendEmailEnabled = sendEmailEnabled;
     this.messageIdGenerator = messageIdGenerator;
   }
 
@@ -63,6 +67,9 @@
       Collection<Address> copiedByEmail,
       Collection<Address> removedByEmail,
       NotifyResolver.Result notify) {
+    if (!sendEmailEnabled) {
+      return;
+    }
     // The user knows they added/removed themselves, don't bother emailing them.
     Account.Id userId = user.getAccountId();
     ImmutableList<Account.Id> immutableToMail =
diff --git a/java/com/google/gerrit/server/config/SendEmailEnabled.java b/java/com/google/gerrit/server/config/SendEmailEnabled.java
new file mode 100644
index 0000000..f52349f
--- /dev/null
+++ b/java/com/google/gerrit/server/config/SendEmailEnabled.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2026 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.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+/** Marker on a {@link Boolean} holding whether email sending is enabled. */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface SendEmailEnabled {}
diff --git a/java/com/google/gerrit/server/config/SendEmailEnabledModule.java b/java/com/google/gerrit/server/config/SendEmailEnabledModule.java
new file mode 100644
index 0000000..9f4e071
--- /dev/null
+++ b/java/com/google/gerrit/server/config/SendEmailEnabledModule.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2026 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.config;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.inject.AbstractModule;
+
+/** Supports binding the {@link SendEmailEnabled} annotation. */
+public class SendEmailEnabledModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(Boolean.class)
+        .annotatedWith(SendEmailEnabled.class)
+        .toProvider(SendEmailEnabledProvider.class)
+        .in(SINGLETON);
+  }
+}
diff --git a/java/com/google/gerrit/server/config/SendEmailEnabledProvider.java b/java/com/google/gerrit/server/config/SendEmailEnabledProvider.java
new file mode 100644
index 0000000..a9a5a39
--- /dev/null
+++ b/java/com/google/gerrit/server/config/SendEmailEnabledProvider.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2026 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.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+/** Provides whether email sending is enabled from {@code sendemail.enable}. */
+@Singleton
+public class SendEmailEnabledProvider implements Provider<Boolean> {
+  private final boolean enabled;
+
+  @Inject
+  public SendEmailEnabledProvider(@GerritServerConfig Config cfg) {
+    enabled = cfg.getBoolean("sendemail", null, "enable", true);
+  }
+
+  @Override
+  public Boolean get() {
+    return enabled;
+  }
+}
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index a3c8deb..3360c3f 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.entities.SubmissionId;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.config.SendEmailEnabled;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.extensions.events.ChangeMerged;
 import com.google.gerrit.server.mail.EmailFactories;
@@ -76,6 +77,7 @@
   private final ExecutorService sendEmailExecutor;
   private final ChangeMerged changeMerged;
   private final MessageIdGenerator messageIdGenerator;
+  private final boolean sendEmailEnabled;
 
   private final PatchSet.Id psId;
   private final SubmissionId submissionId;
@@ -95,6 +97,7 @@
       EmailFactories emailFactories,
       PatchSetUtil psUtil,
       @SendEmailExecutor ExecutorService sendEmailExecutor,
+      @SendEmailEnabled Boolean sendEmailEnabled,
       ChangeMerged changeMerged,
       MessageIdGenerator messageIdGenerator,
       @Assisted RequestScopePropagator requestScopePropagator,
@@ -107,6 +110,7 @@
     this.emailFactories = emailFactories;
     this.psUtil = psUtil;
     this.sendEmailExecutor = sendEmailExecutor;
+    this.sendEmailEnabled = sendEmailEnabled;
     this.changeMerged = changeMerged;
     this.messageIdGenerator = messageIdGenerator;
     this.requestScopePropagator = requestScopePropagator;
@@ -182,41 +186,44 @@
     if (!correctBranch) {
       return;
     }
-    @SuppressWarnings("unused") // Runnable already handles errors
-    Future<?> possiblyIgnoredError =
-        sendEmailExecutor.submit(
-            requestScopePropagator.wrap(
-                new Runnable() {
-                  @Override
-                  public void run() {
-                    try {
-                      // The stickyApprovalDiff is always empty here since this is not supported
-                      // for direct pushes.
-                      ChangeEmail changeEmail =
-                          emailFactories.createChangeEmail(
-                              ctx.getProject(),
-                              psId.changeId(),
-                              emailFactories.createMergedChangeEmail(
-                                  /* stickyApprovalDiff= */ Optional.empty(),
-                                  /* modifiedFiles= */ List.of()));
-                      changeEmail.setPatchSet(patchSet, info);
-                      OutgoingEmail outgoingEmail =
-                          emailFactories.createOutgoingEmail(CHANGE_MERGED, changeEmail);
-                      outgoingEmail.setFrom(ctx.getAccountId());
-                      outgoingEmail.setMessageId(
-                          messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
-                      outgoingEmail.send();
-                    } catch (Exception e) {
-                      logger.atSevere().withCause(e).log(
-                          "Cannot send email for submitted patch set %s", psId);
-                    }
-                  }
 
-                  @Override
-                  public String toString() {
-                    return "send-email merged";
-                  }
-                }));
+    if (sendEmailEnabled) {
+      @SuppressWarnings("unused") // Runnable already handles errors
+      Future<?> possiblyIgnoredError =
+          sendEmailExecutor.submit(
+              requestScopePropagator.wrap(
+                  new Runnable() {
+                    @Override
+                    public void run() {
+                      try {
+                        // The stickyApprovalDiff is always empty here since this is not supported
+                        // for direct pushes.
+                        ChangeEmail changeEmail =
+                            emailFactories.createChangeEmail(
+                                ctx.getProject(),
+                                psId.changeId(),
+                                emailFactories.createMergedChangeEmail(
+                                    /* stickyApprovalDiff= */ Optional.empty(),
+                                    /* modifiedFiles= */ List.of()));
+                        changeEmail.setPatchSet(patchSet, info);
+                        OutgoingEmail outgoingEmail =
+                            emailFactories.createOutgoingEmail(CHANGE_MERGED, changeEmail);
+                        outgoingEmail.setFrom(ctx.getAccountId());
+                        outgoingEmail.setMessageId(
+                            messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
+                        outgoingEmail.send();
+                      } catch (Exception e) {
+                        logger.atSevere().withCause(e).log(
+                            "Cannot send email for submitted patch set %s", psId);
+                      }
+                    }
+
+                    @Override
+                    public String toString() {
+                      return "send-email merged";
+                    }
+                  }));
+    }
 
     changeMerged.fire(
         ctx.getChangeData(change), patchSet, ctx.getAccount(), mergeResultRevId, ctx.getWhen());
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 7e5855d..c050c13 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SendEmailEnabled;
 import com.google.gerrit.server.mail.Encryption;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.AbstractModule;
@@ -91,8 +92,8 @@
   private int expiryDays;
 
   @Inject
-  SmtpEmailSender(@GerritServerConfig Config cfg) {
-    enabled = cfg.getBoolean("sendemail", null, "enable", true);
+  SmtpEmailSender(@GerritServerConfig Config cfg, @SendEmailEnabled Boolean enabled) {
+    this.enabled = enabled;
     connectTimeout =
         Ints.checkedCast(
             ConfigUtil.getTimeUnit(
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index 4bd6f3d..25c7566 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.NotifyResolver;
+import com.google.gerrit.server.config.SendEmailEnabled;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.EmailFactories;
 import com.google.gerrit.server.mail.send.ChangeEmail;
@@ -59,6 +60,7 @@
   private final EmailFactories emailFactories;
   private final ThreadLocalRequestContext requestContext;
   private final MessageIdGenerator messageIdGenerator;
+  private final boolean sendEmailEnabled;
 
   private final Project.NameKey project;
   private final Change change;
@@ -71,6 +73,7 @@
   @Inject
   EmailMerge(
       @SendEmailExecutor ExecutorService executor,
+      @SendEmailEnabled Boolean sendEmailEnabled,
       EmailFactories emailFactories,
       ThreadLocalRequestContext requestContext,
       MessageIdGenerator messageIdGenerator,
@@ -82,6 +85,7 @@
       @Assisted String stickyApprovalDiff,
       @Assisted List<FileDiffOutput> modifiedFiles) {
     this.sendEmailsExecutor = executor;
+    this.sendEmailEnabled = sendEmailEnabled;
     this.emailFactories = emailFactories;
     this.requestContext = requestContext;
     this.messageIdGenerator = messageIdGenerator;
@@ -95,6 +99,9 @@
   }
 
   void sendAsync() {
+    if (!sendEmailEnabled) {
+      return;
+    }
     @SuppressWarnings("unused")
     Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(this);
   }
diff --git a/java/com/google/gerrit/server/util/AttentionSetEmail.java b/java/com/google/gerrit/server/util/AttentionSetEmail.java
index 95fc246..e6a5f3d 100644
--- a/java/com/google/gerrit/server/util/AttentionSetEmail.java
+++ b/java/com/google/gerrit/server/util/AttentionSetEmail.java
@@ -17,12 +17,15 @@
 import static com.google.gerrit.server.mail.EmailFactories.ATTENTION_SET_ADDED;
 import static com.google.gerrit.server.mail.EmailFactories.ATTENTION_SET_REMOVED;
 
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.NotifyResolver;
+import com.google.gerrit.server.config.SendEmailEnabled;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.EmailFactories;
 import com.google.gerrit.server.mail.send.AttentionSetChangeEmailDecorator;
@@ -63,11 +66,13 @@
   }
 
   private final ExecutorService sendEmailsExecutor;
-  private final AsyncSender asyncSender;
+  private final Supplier<AsyncSender> asyncSenderSupplier;
+  private final boolean sendEmailEnabled;
 
   @Inject
   AttentionSetEmail(
       @SendEmailExecutor ExecutorService executor,
+      @SendEmailEnabled Boolean sendEmailEnabled,
       ThreadLocalRequestContext requestContext,
       MessageIdGenerator messageIdGenerator,
       AccountTemplateUtil accountTemplateUtil,
@@ -78,33 +83,40 @@
       @Assisted String reason,
       @Assisted Account.Id attentionUserId) {
     this.sendEmailsExecutor = executor;
+    this.sendEmailEnabled = sendEmailEnabled;
 
-    MessageId messageId;
-    try {
-      messageId =
-          messageIdGenerator.fromChangeUpdateAndReason(
-              ctx.getRepoView(), change.currentPatchSetId(), "AttentionSetEmail");
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
+    this.asyncSenderSupplier =
+        Suppliers.memoize(
+            () -> {
+              MessageId messageId;
+              try {
+                messageId =
+                    messageIdGenerator.fromChangeUpdateAndReason(
+                        ctx.getRepoView(), change.currentPatchSetId(), "AttentionSetEmail");
+              } catch (IOException e) {
+                throw new UncheckedIOException(e);
+              }
 
-    this.asyncSender =
-        new AsyncSender(
-            requestContext,
-            emailFactories,
-            ctx.getUser(),
-            ctx.getProject(),
-            attentionSetChange,
-            messageId,
-            ctx.getNotify(change.getId()),
-            attentionUserId,
-            accountTemplateUtil.replaceTemplates(reason),
-            change.getId());
+              return new AsyncSender(
+                  requestContext,
+                  emailFactories,
+                  ctx.getUser(),
+                  ctx.getProject(),
+                  attentionSetChange,
+                  messageId,
+                  ctx.getNotify(change.getId()),
+                  attentionUserId,
+                  accountTemplateUtil.replaceTemplates(reason),
+                  change.getId());
+            });
   }
 
   public void sendAsync() {
+    if (!sendEmailEnabled) {
+      return;
+    }
     @SuppressWarnings("unused")
-    Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(asyncSender);
+    Future<?> possiblyIgnoredError = sendEmailsExecutor.submit(asyncSenderSupplier.get());
   }
 
   /**
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 657e1f2..745f89a 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -87,6 +87,7 @@
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.config.GerritServerIdProvider;
 import com.google.gerrit.server.config.GlobalPluginConfigProvider;
+import com.google.gerrit.server.config.SendEmailEnabledModule;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.TrackingFooters;
@@ -267,6 +268,7 @@
     install(NoSshKeyCache.module());
     install(new GerritInstanceNameModule());
     install(new GerritInstanceIdModule());
+    install(new SendEmailEnabledModule());
     install(
         new CanonicalWebUrlModule() {
           @Override
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index a44d2c8..4b0c1ab 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -717,7 +717,12 @@
   if (excludePath.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) return '';
   if (excludePath.line === FILE) return FILE;
   if (excludePath.line) return `#${excludePath.line}`;
-  if (excludePath.range) return `#${excludePath.range.end_line}`;
+  if (excludePath.range) {
+    // If the range is wrong, we display the start line. Happens to AI generated comments.
+    if (excludePath.range.end_line < excludePath.range.start_line)
+      return `#${excludePath.range.start_line}`;
+    return `#${excludePath.range.end_line}`;
+  }
   return '';
 }
 
diff --git a/polygerrit-ui/app/utils/comment-util_test.ts b/polygerrit-ui/app/utils/comment-util_test.ts
index 61d374b..e8fe606 100644
--- a/polygerrit-ui/app/utils/comment-util_test.ts
+++ b/polygerrit-ui/app/utils/comment-util_test.ts
@@ -730,6 +730,20 @@
       );
     });
 
+    test('invalid range', () => {
+      assert.equal(
+        computeDisplayLine({
+          range: {
+            start_line: 12,
+            start_character: 1,
+            end_line: 1,
+            end_character: 10,
+          },
+        }),
+        '#12'
+      );
+    });
+
     test('empty', () => {
       assert.equal(computeDisplayLine({}), '');
     });