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(