From 10d28efa1c7c59c6eaba18e7701502b5ee56bcb9 Mon Sep 17 00:00:00 2001 From: Weimin Yu Date: Wed, 2 Aug 2023 16:09:45 -0400 Subject: [PATCH] Add placeholder configs for Gmail (#2089) Add placeholder configs for sending emails using Gmail in GSuite. The names of the new configs are temporary. After migration they will revert to the names currently in use by the AppEngine email API. --- .../batch/CannedScriptExecutionAction.java | 4 +- .../registry/config/CredentialModule.java | 56 ++++++++++++++++++- .../registry/config/RegistryConfig.java | 25 +++++++++ .../config/RegistryConfigSettings.java | 5 ++ .../registry/config/files/default-config.yaml | 6 ++ .../google/registry/groups/GmailClient.java | 24 +++++++- .../google/registry/groups/GmailModule.java | 4 +- .../google/registry/util/EmailMessage.java | 1 + 8 files changed, 117 insertions(+), 8 deletions(-) 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();