diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java
index 8ee224a98..355d9373e 100644
--- a/core/src/main/java/google/registry/config/RegistryConfig.java
+++ b/core/src/main/java/google/registry/config/RegistryConfig.java
@@ -879,6 +879,17 @@ public final class RegistryConfig {
return Optional.ofNullable(config.misc.sheetExportId);
}
+ /**
+ * Returns the desired delay between outgoing emails when sending in bulk.
+ *
+ *
Gmail apparently has unpublished limits on peak throughput over short period.
+ */
+ @Provides
+ @Config("emailThrottleDuration")
+ public static Duration provideEmailThrottleSeconds(RegistryConfigSettings config) {
+ return Duration.standardSeconds(config.misc.emailThrottleSeconds);
+ }
+
/**
* Returns the email address we send various alert e-mails to.
*
diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java
index cb21f4147..b9b424c6c 100644
--- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java
+++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java
@@ -208,6 +208,7 @@ public class RegistryConfigSettings {
public static class Misc {
public String sheetExportId;
public boolean isEmailSendingEnabled;
+ public int emailThrottleSeconds;
public String alertRecipientEmailAddress;
// TODO(b/279671974): remove below field after migration
public String newAlertRecipientEmailAddress;
diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml
index c9a58f268..1d772f5de 100644
--- a/core/src/main/java/google/registry/config/files/default-config.yaml
+++ b/core/src/main/java/google/registry/config/files/default-config.yaml
@@ -443,6 +443,9 @@ misc:
# Whether emails may be sent. For Prod and Sandbox this should be true.
isEmailSendingEnabled: false
+ # Delay between bulk messages to avoid triggering Gmail fraud checks
+ emailThrottleSeconds: 30
+
# Address we send alert summary emails to.
alertRecipientEmailAddress: email@example.com
diff --git a/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java b/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java
index 9e230fb07..ce88c7be6 100644
--- a/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java
+++ b/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java
@@ -53,7 +53,7 @@ public class BillingEmailUtils {
GmailClient gmailClient,
YearMonth yearMonth,
@Config("gSuiteOutgoingEmailAddress") InternetAddress outgoingEmailAddress,
- @Config("alertRecipientEmailAddress") InternetAddress alertRecipientAddress,
+ @Config("newAlertRecipientEmailAddress") InternetAddress alertRecipientAddress,
@Config("invoiceEmailRecipients") ImmutableList invoiceEmailRecipients,
@Config("invoiceReplyToEmailAddress") Optional replyToEmailAddress,
@Config("billingBucket") String billingBucket,
diff --git a/core/src/main/java/google/registry/reporting/spec11/Spec11EmailUtils.java b/core/src/main/java/google/registry/reporting/spec11/Spec11EmailUtils.java
index 9bf55a254..a91716c2b 100644
--- a/core/src/main/java/google/registry/reporting/spec11/Spec11EmailUtils.java
+++ b/core/src/main/java/google/registry/reporting/spec11/Spec11EmailUtils.java
@@ -37,11 +37,13 @@ import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo;
import google.registry.util.EmailMessage;
+import google.registry.util.Sleeper;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
+import org.joda.time.Duration;
import org.joda.time.LocalDate;
/** Provides e-mail functionality for Spec11 tasks, such as sending Spec11 reports to registrars. */
@@ -57,6 +59,8 @@ public class Spec11EmailUtils {
.build()
.compileToTofu();
private final GmailClient gmailClient;
+ private final Sleeper sleeper;
+ private final Duration emailThrottleDuration;
private final InternetAddress outgoingEmailAddress;
private final ImmutableList spec11BccEmailAddresses;
private final InternetAddress alertRecipientAddress;
@@ -66,12 +70,16 @@ public class Spec11EmailUtils {
@Inject
Spec11EmailUtils(
GmailClient gmailClient,
+ Sleeper sleeper,
+ @Config("emailThrottleDuration") Duration emailThrottleDuration,
@Config("newAlertRecipientEmailAddress") InternetAddress alertRecipientAddress,
@Config("spec11OutgoingEmailAddress") InternetAddress spec11OutgoingEmailAddress,
@Config("spec11BccEmailAddresses") ImmutableList spec11BccEmailAddresses,
@Config("spec11WebResources") ImmutableList spec11WebResources,
@Config("registryName") String registryName) {
this.gmailClient = gmailClient;
+ this.sleeper = sleeper;
+ this.emailThrottleDuration = emailThrottleDuration;
this.outgoingEmailAddress = spec11OutgoingEmailAddress;
this.spec11BccEmailAddresses = spec11BccEmailAddresses;
this.alertRecipientAddress = alertRecipientAddress;
@@ -94,6 +102,13 @@ public class Spec11EmailUtils {
for (RegistrarThreatMatches registrarThreatMatches : registrarThreatMatchesSet) {
RegistrarThreatMatches filteredMatches = filterOutNonPublishedMatches(registrarThreatMatches);
if (!filteredMatches.threatMatches().isEmpty()) {
+ if (numRegistrarsEmailed > 0) {
+ try {
+ sleeper.sleep(emailThrottleDuration);
+ } catch (InterruptedException ie) {
+ throw new RuntimeException(ie);
+ }
+ }
try {
// Handle exceptions individually per registrar so that one failed email doesn't prevent
// the rest from being sent.
diff --git a/core/src/test/java/google/registry/reporting/spec11/Spec11EmailUtilsTest.java b/core/src/test/java/google/registry/reporting/spec11/Spec11EmailUtilsTest.java
index ce6511f6f..75071911e 100644
--- a/core/src/test/java/google/registry/reporting/spec11/Spec11EmailUtilsTest.java
+++ b/core/src/test/java/google/registry/reporting/spec11/Spec11EmailUtilsTest.java
@@ -26,6 +26,8 @@ import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -41,11 +43,13 @@ import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationT
import google.registry.reporting.spec11.soy.Spec11EmailSoyInfo;
import google.registry.testing.DatabaseHelper;
import google.registry.util.EmailMessage;
+import google.registry.util.Sleeper;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
+import org.joda.time.Duration;
import org.joda.time.LocalDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -101,6 +105,8 @@ class Spec11EmailUtilsTest {
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@Mock private GmailClient gmailClient;
+ @Mock private Sleeper sleeper;
+ private Duration emailThrottleDuration = Duration.millis(1);
private Spec11EmailUtils emailUtils;
private ArgumentCaptor contentCaptor;
private final LocalDate date = new LocalDate(2018, 7, 15);
@@ -114,6 +120,8 @@ class Spec11EmailUtilsTest {
emailUtils =
new Spec11EmailUtils(
gmailClient,
+ sleeper,
+ emailThrottleDuration,
new InternetAddress("my-receiver@test.com"),
new InternetAddress("abuse@test.com"),
ImmutableList.of(
@@ -128,6 +136,19 @@ class Spec11EmailUtilsTest {
persistDomainWithHost("c.com", host);
}
+ @Test
+ void testSuccess_sleepsBetweenSending() throws Exception {
+ emailUtils.emailSpec11Reports(
+ date,
+ Spec11EmailSoyInfo.MONTHLY_SPEC_11_EMAIL,
+ "Super Cool Registry Monthly Threat Detector [2018-07-15]",
+ sampleThreatMatches());
+ // We inspect individual parameters because Message doesn't implement equals().
+ verify(gmailClient, times(3)).sendEmail(any(EmailMessage.class));
+ // Sleep once between two reports sent in a tight loop. No sleep before the final alert message.
+ verify(sleeper, times(1)).sleep(same(emailThrottleDuration));
+ }
+
@Test
void testSuccess_emailMonthlySpec11Reports() throws Exception {
emailUtils.emailSpec11Reports(