diff --git a/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java b/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java index 2558f4a4b..eb1357af5 100644 --- a/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java +++ b/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java @@ -71,7 +71,7 @@ public class CannedScriptExecutionAction implements Runnable { GmailClient gmailClient, @Config("projectId") String projectId, @Config("gSuiteDomainName") String gSuiteDomainName, - @Config("alertRecipientEmailAddress") InternetAddress recipientAddress) { + @Config("newAlertRecipientEmailAddress") InternetAddress recipientAddress) { this.groupsConnection = groupsConnection; this.gmailClient = gmailClient; this.gSuiteDomainName = gSuiteDomainName; @@ -116,6 +116,8 @@ public class CannedScriptExecutionAction implements Runnable { try { Set currentMembers = groupsConnection.getMembersOfGroup(groupKey); logger.atInfo().log("%s has %s members.", groupKey, currentMembers.size()); + // One success is enough for validation. + return; } catch (IOException e) { logger.atWarning().withCause(e).log("Failed to check %s", groupKey); } diff --git a/core/src/main/java/google/registry/config/CredentialModule.java b/core/src/main/java/google/registry/config/CredentialModule.java index 5d57d25ae..25e1170af 100644 --- a/core/src/main/java/google/registry/config/CredentialModule.java +++ b/core/src/main/java/google/registry/config/CredentialModule.java @@ -93,13 +93,56 @@ public abstract class CredentialModule { @AdcDelegatedCredential @Provides @Singleton - public static GoogleCredentialsBundle provideSelfSignedDelegatedCredential( + public static GoogleCredentialsBundle provideSelfSignedAdminDelegatedCredential( @Config("defaultCredentialOauthScopes") ImmutableList defaultScopes, @Config("delegatedCredentialOauthScopes") ImmutableList delegationScopes, @ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle, @Config("gSuiteAdminAccountEmailAddress") String gSuiteAdminAccountEmailAddress, @Config("tokenRefreshDelay") Duration tokenRefreshDelay, Clock clock) { + return createSelfSignedDelegatedCredential( + defaultScopes, + delegationScopes, + credentialsBundle, + gSuiteAdminAccountEmailAddress, + tokenRefreshDelay, + clock); + } + + /** + * Provides a {@link GoogleCredentialsBundle} for sending emails through Google Workspace. + * + *

The Workspace domain must grant delegated admin access to the default service account user + * (project-id@appspot.gserviceaccount.com on AppEngine) with all scopes in {@code defaultScopes} + * and {@code delegationScopes}. In addition, the user {@code gSuiteOutgoingEmailAddress} must + * have the permission to send emails. + */ + @GmailDelegatedCredential + @Provides + @Singleton + public static GoogleCredentialsBundle provideSelfSignedGmailDelegatedCredential( + @Config("defaultCredentialOauthScopes") ImmutableList defaultScopes, + @Config("delegatedCredentialOauthScopes") ImmutableList delegationScopes, + @ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle, + @Config("gSuiteNewOutgoingEmailAddress") String gSuiteOutgoingEmailAddress, + @Config("tokenRefreshDelay") Duration tokenRefreshDelay, + Clock clock) { + return createSelfSignedDelegatedCredential( + defaultScopes, + delegationScopes, + credentialsBundle, + gSuiteOutgoingEmailAddress, + tokenRefreshDelay, + clock); + } + + public static GoogleCredentialsBundle createSelfSignedDelegatedCredential( + ImmutableList defaultScopes, + ImmutableList delegationScopes, + GoogleCredentialsBundle credentialsBundle, + String gSuiteUserEmailAddress, + Duration tokenRefreshDelay, + Clock clock) { GoogleCredentials signer = credentialsBundle.getGoogleCredentials(); checkArgument( @@ -118,7 +161,7 @@ public abstract class CredentialModule { DelegatedCredentials.createSelfSignedDelegatedCredential( (ServiceAccountSigner) signer, ImmutableList.builder().addAll(defaultScopes).addAll(delegationScopes).build(), - gSuiteAdminAccountEmailAddress, + gSuiteUserEmailAddress, clock, tokenRefreshDelay); return GoogleCredentialsBundle.create(credential); @@ -136,6 +179,15 @@ public abstract class CredentialModule { @Retention(RetentionPolicy.RUNTIME) public @interface GoogleWorkspaceCredential {} + /** + * Dagger qualifier for a credential with delegated Email-sending permission for a dasher domain + * (for Google Workspace) backed by the application default credential (ADC). + */ + @Qualifier + @Documented + @Retention(RetentionPolicy.RUNTIME) + public @interface GmailDelegatedCredential {} + /** * Dagger qualifier for a credential with delegated admin access for a dasher domain (for Google * Workspace) backed by the application default credential (ADC). diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index c71f55e81..b991aab5e 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -538,6 +538,13 @@ public final class RegistryConfig { return parseEmailAddress(config.gSuite.outgoingEmailAddress); } + // TODO(b/279671974): reuse the 'gSuiteOutgoingEmailAddress' annotation after migration + @Provides + @Config("gSuiteNewOutgoingEmailAddress") + public static String provideGSuiteNewOutgoingEmailAddress(RegistryConfigSettings config) { + return config.gSuite.newOutgoingEmailAddress; + } + /** * The display name that is used on outgoing emails sent by Nomulus. * @@ -549,6 +556,16 @@ public final class RegistryConfig { return config.gSuite.outgoingEmailDisplayName; } + /** + * Provides the `reply-to` address for outgoing email messages. This address may be outside the + * GSuite domain. + */ + @Provides + @Config("replyToEmailAddress") + public static InternetAddress provideReplyToEmailAddress(RegistryConfigSettings config) { + return parseEmailAddress(config.gSuite.replyToEmailAddress); + } + /** * Returns whether an SSL certificate hash is required to log in via EPP and run flows. * @@ -859,6 +876,14 @@ public final class RegistryConfig { return parseEmailAddress(config.misc.alertRecipientEmailAddress); } + // TODO(b/279671974): remove below method after migration + @Provides + @Config("newAlertRecipientEmailAddress") + public static InternetAddress provideNewAlertRecipientEmailAddress( + RegistryConfigSettings config) { + return parseEmailAddress(config.misc.newAlertRecipientEmailAddress); + } + /** * Returns the email address to which spec 11 email should be replied. * diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index a02fe78c9..5430e260d 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -77,6 +77,9 @@ public class RegistryConfigSettings { public static class GSuite { public String domainName; public String outgoingEmailAddress; + // TODO(b/279671974): remove below field after migration + public String newOutgoingEmailAddress; + public String replyToEmailAddress; public String outgoingEmailDisplayName; public String adminAccountEmailAddress; public String supportGroupEmailAddress; @@ -203,6 +206,8 @@ public class RegistryConfigSettings { public static class Misc { public String sheetExportId; public String alertRecipientEmailAddress; + // TODO(b/279671974): remove below field after migration + public String newAlertRecipientEmailAddress; public String spec11OutgoingEmailAddress; public List spec11BccEmailAddresses; public int transientFailureRetries; 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 347a2c532..f872567fc 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 @@ -33,6 +33,9 @@ gSuite: # https://cloud.google.com/appengine/docs/standard/java/mail/#who_can_send_mail outgoingEmailDisplayName: Example Registry outgoingEmailAddress: noreply@project-id.appspotmail.com + # TODO(b/279671974): reuse `outgoingEmailAddress` after migration + newOutgoingEmailAddress: noreply@example.com + replyToEmailAddress: reply-to@example.com # Email address of the admin account on the G Suite app. This is used for # logging in to perform administrative actions, not sending emails. @@ -432,6 +435,9 @@ misc: # Address we send alert summary emails to. alertRecipientEmailAddress: email@example.com + # TODO(b/279671974): reuse `alertRecipientEmailAddress` after migration + newAlertRecipientEmailAddress: email@example.com + # Address from which Spec 11 emails to registrars are sent. This needs # to be a deliverable email address to handle replies from registrars as well. spec11OutgoingEmailAddress: abuse@example.com diff --git a/core/src/main/java/google/registry/groups/GmailClient.java b/core/src/main/java/google/registry/groups/GmailClient.java index aca725bfe..131750973 100644 --- a/core/src/main/java/google/registry/groups/GmailClient.java +++ b/core/src/main/java/google/registry/groups/GmailClient.java @@ -20,10 +20,12 @@ import com.google.api.services.gmail.Gmail; import com.google.api.services.gmail.model.Message; import com.google.common.net.MediaType; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import google.registry.config.RegistryConfig.Config; import google.registry.util.EmailMessage; import google.registry.util.EmailMessage.Attachment; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.util.Properties; import javax.inject.Inject; import javax.mail.Address; @@ -42,10 +44,24 @@ import org.apache.commons.codec.binary.Base64; public final class GmailClient { private final Gmail gmail; + private final InternetAddress outgoingEmailAddressWithUsername; + private final InternetAddress replyToEmailAddress; @Inject - GmailClient(Gmail gmail) { + GmailClient( + Gmail gmail, + @Config("gSuiteNewOutgoingEmailAddress") String gSuiteOutgoingEmailAddress, + @Config("gSuiteOutgoingEmailDisplayName") String gSuiteOutgoingEmailDisplayName, + @Config("replyToEmailAddress") InternetAddress replyToEmailAddress) { + this.gmail = gmail; + this.replyToEmailAddress = replyToEmailAddress; + try { + this.outgoingEmailAddressWithUsername = + new InternetAddress(gSuiteOutgoingEmailAddress, gSuiteOutgoingEmailDisplayName); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } } /** @@ -58,6 +74,7 @@ public final class GmailClient { public Message sendEmail(EmailMessage emailMessage) { Message message = toGmailMessage(toMimeMessage(emailMessage)); try { + // "me" is reserved word for the authorized user of the Gmail API. return gmail.users().messages().send("me", message).execute(); } catch (IOException e) { throw new EmailException(e); @@ -78,11 +95,12 @@ public final class GmailClient { } } - static MimeMessage toMimeMessage(EmailMessage emailMessage) { + MimeMessage toMimeMessage(EmailMessage emailMessage) { try { MimeMessage msg = new MimeMessage(Session.getDefaultInstance(new Properties(), /* authenticator= */ null)); - msg.setFrom(emailMessage.from()); + msg.setFrom(this.outgoingEmailAddressWithUsername); + msg.setReplyTo(new InternetAddress[] {replyToEmailAddress}); msg.addRecipients( RecipientType.TO, toArray(emailMessage.recipients(), InternetAddress.class)); msg.setSubject(emailMessage.subject()); diff --git a/core/src/main/java/google/registry/groups/GmailModule.java b/core/src/main/java/google/registry/groups/GmailModule.java index 28f4a3f8e..d7b241520 100644 --- a/core/src/main/java/google/registry/groups/GmailModule.java +++ b/core/src/main/java/google/registry/groups/GmailModule.java @@ -17,7 +17,7 @@ package google.registry.groups; import com.google.api.services.gmail.Gmail; import dagger.Module; import dagger.Provides; -import google.registry.config.CredentialModule.AdcDelegatedCredential; +import google.registry.config.CredentialModule.GmailDelegatedCredential; import google.registry.config.RegistryConfig.Config; import google.registry.util.GoogleCredentialsBundle; import javax.inject.Singleton; @@ -29,7 +29,7 @@ public class GmailModule { @Provides @Singleton Gmail provideGmail( - @AdcDelegatedCredential GoogleCredentialsBundle credentialsBundle, + @GmailDelegatedCredential GoogleCredentialsBundle credentialsBundle, @Config("projectId") String projectId) { return new Gmail.Builder( credentialsBundle.getHttpTransport(), diff --git a/util/src/main/java/google/registry/util/EmailMessage.java b/util/src/main/java/google/registry/util/EmailMessage.java index ac63d31d9..7c0112a41 100644 --- a/util/src/main/java/google/registry/util/EmailMessage.java +++ b/util/src/main/java/google/registry/util/EmailMessage.java @@ -46,6 +46,7 @@ public abstract class EmailMessage { public abstract ImmutableSet recipients(); + // TODO(b/279671974): remove `from` after migration. public abstract InternetAddress from(); public abstract ImmutableSet ccs();