diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 9f40c866b..8ee224a98 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -723,6 +723,18 @@ public final class RegistryConfig { .collect(toImmutableList()); } + /** + * Returns an optional return email address that overrides the default {@code reply-to} address + * in outgoing invoicing email messages. + */ + @Provides + @Config("invoiceReplyToEmailAddress") + public static Optional provideInvoiceReplyToEmailAddress( + RegistryConfigSettings config) { + return Optional.ofNullable(config.billing.invoiceReplyToEmailAddress) + .map(RegistryConfig::parseEmailAddress); + } + /** * Returns the file prefix for the invoice CSV file. * diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 5cb630cdd..cb21f4147 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -170,6 +170,7 @@ public class RegistryConfigSettings { /** Configuration for monthly invoices. */ public static class Billing { public List invoiceEmailRecipients; + public String invoiceReplyToEmailAddress; public String invoiceFilePrefix; } 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 35842416f..c9a58f268 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 @@ -382,6 +382,8 @@ icannReporting: billing: invoiceEmailRecipients: [] + # Optional return address that overrides the default. + invoiceReplyToEmailAddress: null invoiceFilePrefix: REG-INV rde: diff --git a/core/src/main/java/google/registry/groups/GmailClient.java b/core/src/main/java/google/registry/groups/GmailClient.java index 42f46971b..569456784 100644 --- a/core/src/main/java/google/registry/groups/GmailClient.java +++ b/core/src/main/java/google/registry/groups/GmailClient.java @@ -120,7 +120,8 @@ public final class GmailClient { MimeMessage msg = new MimeMessage(Session.getDefaultInstance(new Properties(), /* authenticator= */ null)); msg.setFrom(this.outgoingEmailAddressWithUsername); - msg.setReplyTo(new InternetAddress[] {replyToEmailAddress}); + msg.setReplyTo( + new InternetAddress[] {emailMessage.replyToEmailAddress().orElse(replyToEmailAddress)}); msg.addRecipients( RecipientType.TO, toArray(emailMessage.recipients(), InternetAddress.class)); msg.setSubject(emailMessage.subject()); 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 324a9315e..9e230fb07 100644 --- a/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java +++ b/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java @@ -23,12 +23,13 @@ import com.google.common.io.CharStreams; import com.google.common.net.MediaType; import google.registry.config.RegistryConfig.Config; import google.registry.gcs.GcsUtils; +import google.registry.groups.GmailClient; import google.registry.reporting.billing.BillingModule.InvoiceDirectoryPrefix; import google.registry.util.EmailMessage; import google.registry.util.EmailMessage.Attachment; -import google.registry.util.SendEmailService; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.Optional; import javax.inject.Inject; import javax.mail.internet.InternetAddress; import org.joda.time.YearMonth; @@ -36,11 +37,12 @@ import org.joda.time.YearMonth; /** Utility functions for sending emails involving monthly invoices. */ public class BillingEmailUtils { - private final SendEmailService emailService; + private final GmailClient gmailClient; private final YearMonth yearMonth; private final InternetAddress outgoingEmailAddress; private final InternetAddress alertRecipientAddress; private final ImmutableList invoiceEmailRecipients; + private final Optional replyToEmailAddress; private final String billingBucket; private final String invoiceFilePrefix; private final String invoiceDirectoryPrefix; @@ -48,20 +50,22 @@ public class BillingEmailUtils { @Inject BillingEmailUtils( - SendEmailService emailService, + GmailClient gmailClient, YearMonth yearMonth, @Config("gSuiteOutgoingEmailAddress") InternetAddress outgoingEmailAddress, @Config("alertRecipientEmailAddress") InternetAddress alertRecipientAddress, @Config("invoiceEmailRecipients") ImmutableList invoiceEmailRecipients, + @Config("invoiceReplyToEmailAddress") Optional replyToEmailAddress, @Config("billingBucket") String billingBucket, @Config("invoiceFilePrefix") String invoiceFilePrefix, @InvoiceDirectoryPrefix String invoiceDirectoryPrefix, GcsUtils gcsUtils) { - this.emailService = emailService; + this.gmailClient = gmailClient; this.yearMonth = yearMonth; this.outgoingEmailAddress = outgoingEmailAddress; this.alertRecipientAddress = alertRecipientAddress; this.invoiceEmailRecipients = invoiceEmailRecipients; + this.replyToEmailAddress = replyToEmailAddress; this.billingBucket = billingBucket; this.invoiceFilePrefix = invoiceFilePrefix; this.invoiceDirectoryPrefix = invoiceDirectoryPrefix; @@ -74,13 +78,14 @@ public class BillingEmailUtils { String invoiceFile = String.format("%s-%s.csv", invoiceFilePrefix, yearMonth); BlobId invoiceFilename = BlobId.of(billingBucket, invoiceDirectoryPrefix + invoiceFile); try (InputStream in = gcsUtils.openInputStream(invoiceFilename)) { - emailService.sendEmail( + gmailClient.sendEmail( EmailMessage.newBuilder() .setSubject(String.format("Domain Registry invoice data %s", yearMonth)) .setBody( String.format("Attached is the %s invoice for the domain registry.", yearMonth)) .setFrom(outgoingEmailAddress) .setRecipients(invoiceEmailRecipients) + .setReplyToEmailAddress(replyToEmailAddress) .setAttachment( Attachment.newBuilder() .setContent(CharStreams.toString(new InputStreamReader(in, UTF_8))) @@ -100,7 +105,7 @@ public class BillingEmailUtils { /** Sends an e-mail to the provided alert e-mail address indicating a billing failure. */ void sendAlertEmail(String body) { try { - emailService.sendEmail( + gmailClient.sendEmail( EmailMessage.newBuilder() .setSubject(String.format("Billing Pipeline Alert: %s", yearMonth)) .setBody(body) diff --git a/core/src/test/java/google/registry/groups/GmailClientTest.java b/core/src/test/java/google/registry/groups/GmailClientTest.java index e43a095cf..ea720ff37 100644 --- a/core/src/test/java/google/registry/groups/GmailClientTest.java +++ b/core/src/test/java/google/registry/groups/GmailClientTest.java @@ -125,6 +125,9 @@ public class GmailClientTest { assertThat(mimeMessage.getRecipients(RecipientType.TO)).asList().containsExactly(toAddr); assertThat(mimeMessage.getRecipients(RecipientType.CC)).asList().containsExactly(ccAddr); assertThat(mimeMessage.getRecipients(RecipientType.BCC)).asList().containsExactly(bccAddr); + assertThat(mimeMessage.getReplyTo()) + .asList() + .containsExactly(new InternetAddress("replyTo@example.com")); assertThat(mimeMessage.getSubject()).isEqualTo("My subject"); assertThat(mimeMessage.getContent()).isInstanceOf(MimeMultipart.class); MimeMultipart parts = (MimeMultipart) mimeMessage.getContent(); @@ -137,6 +140,23 @@ public class GmailClientTest { assertThat(attachment.getContent()).isEqualTo("foo,bar\nbaz,qux"); } + @Test + public void toMimeMessage_overrideReplyToAddr() throws Exception { + InternetAddress fromAddr = new InternetAddress("from@example.com", "My sender"); + InternetAddress toAddr = new InternetAddress("to@example.com"); + InternetAddress replyToAddr = new InternetAddress("some-addr@another.com"); + EmailMessage emailMessage = + EmailMessage.newBuilder() + .setFrom(fromAddr) + .setRecipients(ImmutableList.of(toAddr)) + .setReplyToEmailAddress(replyToAddr) + .setSubject("My subject") + .setBody("My body") + .build(); + MimeMessage mimeMessage = getGmailClient(true).toMimeMessage(emailMessage); + assertThat(mimeMessage.getReplyTo()).asList().containsExactly(replyToAddr); + } + @Test public void toGmailMessage() throws Exception { MimeMessage mimeMessage = mock(MimeMessage.class); diff --git a/core/src/test/java/google/registry/reporting/billing/BillingEmailUtilsTest.java b/core/src/test/java/google/registry/reporting/billing/BillingEmailUtilsTest.java index e43dbd22f..4872c5edc 100644 --- a/core/src/test/java/google/registry/reporting/billing/BillingEmailUtilsTest.java +++ b/core/src/test/java/google/registry/reporting/billing/BillingEmailUtilsTest.java @@ -27,11 +27,12 @@ import com.google.cloud.storage.BlobId; import com.google.common.collect.ImmutableList; import com.google.common.net.MediaType; import google.registry.gcs.GcsUtils; +import google.registry.groups.GmailClient; import google.registry.util.EmailMessage; import google.registry.util.EmailMessage.Attachment; -import google.registry.util.SendEmailService; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; +import java.util.Optional; import javax.mail.MessagingException; import javax.mail.internet.InternetAddress; import org.joda.time.YearMonth; @@ -42,39 +43,44 @@ import org.mockito.ArgumentCaptor; /** Unit tests for {@link google.registry.reporting.billing.BillingEmailUtils}. */ class BillingEmailUtilsTest { - private SendEmailService emailService; + private GmailClient gmailClient; private BillingEmailUtils emailUtils; private GcsUtils gcsUtils; private ArgumentCaptor contentCaptor; @BeforeEach void beforeEach() throws Exception { - emailService = mock(SendEmailService.class); + gmailClient = mock(GmailClient.class); gcsUtils = mock(GcsUtils.class); when(gcsUtils.openInputStream(BlobId.of("test-bucket", "results/REG-INV-2017-10.csv"))) .thenReturn( new ByteArrayInputStream("test,data\nhello,world".getBytes(StandardCharsets.UTF_8))); contentCaptor = ArgumentCaptor.forClass(EmailMessage.class); - emailUtils = - new BillingEmailUtils( - emailService, - new YearMonth(2017, 10), - new InternetAddress("my-sender@test.com"), - new InternetAddress("my-receiver@test.com"), - ImmutableList.of( - new InternetAddress("hello@world.com"), new InternetAddress("hola@mundo.com")), - "test-bucket", - "REG-INV", - "results/", - gcsUtils); + emailUtils = getEmailUtils(Optional.of(new InternetAddress("reply-to@test.com"))); + } + + private BillingEmailUtils getEmailUtils(Optional replyToAddress) + throws Exception { + return new BillingEmailUtils( + gmailClient, + new YearMonth(2017, 10), + new InternetAddress("my-sender@test.com"), + new InternetAddress("my-receiver@test.com"), + ImmutableList.of( + new InternetAddress("hello@world.com"), new InternetAddress("hola@mundo.com")), + replyToAddress, + "test-bucket", + "REG-INV", + "results/", + gcsUtils); } @Test void testSuccess_emailOverallInvoice() throws MessagingException { emailUtils.emailOverallInvoice(); - verify(emailService).sendEmail(contentCaptor.capture()); + verify(gmailClient).sendEmail(contentCaptor.capture()); EmailMessage emailMessage = contentCaptor.getValue(); EmailMessage expectedContent = EmailMessage.newBuilder() @@ -84,6 +90,7 @@ class BillingEmailUtilsTest { new InternetAddress("hello@world.com"), new InternetAddress("hola@mundo.com"))) .setSubject("Domain Registry invoice data 2017-10") .setBody("Attached is the 2017-10 invoice for the domain registry.") + .setReplyToEmailAddress(new InternetAddress("reply-to@test.com")) .setAttachment( Attachment.newBuilder() .setContent("test,data\nhello,world") @@ -94,11 +101,21 @@ class BillingEmailUtilsTest { assertThat(emailMessage).isEqualTo(expectedContent); } + @Test + void testSuccess_emailOverallInvoiceNoReplyOverride() throws Exception { + emailUtils = getEmailUtils(Optional.empty()); + emailUtils.emailOverallInvoice(); + + verify(gmailClient).sendEmail(contentCaptor.capture()); + EmailMessage emailMessage = contentCaptor.getValue(); + assertThat(emailMessage.replyToEmailAddress()).isEmpty(); + } + @Test void testFailure_emailsAlert() throws MessagingException { doThrow(new RuntimeException(new MessagingException("expected"))) .doNothing() - .when(emailService) + .when(gmailClient) .sendEmail(contentCaptor.capture()); RuntimeException thrown = assertThrows(RuntimeException.class, () -> emailUtils.emailOverallInvoice()); @@ -108,14 +125,14 @@ class BillingEmailUtilsTest { .hasMessageThat() .isEqualTo("javax.mail.MessagingException: expected"); // Verify we sent an e-mail alert - verify(emailService, times(2)).sendEmail(contentCaptor.capture()); + verify(gmailClient, times(2)).sendEmail(contentCaptor.capture()); validateAlertMessage(contentCaptor.getValue(), "Emailing invoice failed due to expected"); } @Test void testSuccess_sendAlertEmail() throws MessagingException { emailUtils.sendAlertEmail("Alert!"); - verify(emailService).sendEmail(contentCaptor.capture()); + verify(gmailClient).sendEmail(contentCaptor.capture()); validateAlertMessage(contentCaptor.getValue(), "Alert!"); } diff --git a/util/src/main/java/google/registry/util/EmailMessage.java b/util/src/main/java/google/registry/util/EmailMessage.java index 7c0112a41..15e944231 100644 --- a/util/src/main/java/google/registry/util/EmailMessage.java +++ b/util/src/main/java/google/registry/util/EmailMessage.java @@ -49,6 +49,9 @@ public abstract class EmailMessage { // TODO(b/279671974): remove `from` after migration. public abstract InternetAddress from(); + /** Optional return email address that overrides the default. */ + public abstract Optional replyToEmailAddress(); + public abstract ImmutableSet ccs(); public abstract ImmutableSet bccs(); @@ -69,6 +72,10 @@ public abstract class EmailMessage { public abstract Builder setFrom(InternetAddress from); + public abstract Builder setReplyToEmailAddress(InternetAddress replyToEmailAddress); + + public abstract Builder setReplyToEmailAddress(Optional replyToEmailAddress); + public abstract Builder setBccs(Collection bccs); public abstract Builder setCcs(Collection ccs);