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.
This commit is contained in:
Weimin Yu 2023-08-02 16:09:45 -04:00 committed by GitHub
parent 1e0a0cf29e
commit 10d28efa1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 117 additions and 8 deletions

View file

@ -71,7 +71,7 @@ public class CannedScriptExecutionAction implements Runnable {
GmailClient gmailClient, GmailClient gmailClient,
@Config("projectId") String projectId, @Config("projectId") String projectId,
@Config("gSuiteDomainName") String gSuiteDomainName, @Config("gSuiteDomainName") String gSuiteDomainName,
@Config("alertRecipientEmailAddress") InternetAddress recipientAddress) { @Config("newAlertRecipientEmailAddress") InternetAddress recipientAddress) {
this.groupsConnection = groupsConnection; this.groupsConnection = groupsConnection;
this.gmailClient = gmailClient; this.gmailClient = gmailClient;
this.gSuiteDomainName = gSuiteDomainName; this.gSuiteDomainName = gSuiteDomainName;
@ -116,6 +116,8 @@ public class CannedScriptExecutionAction implements Runnable {
try { try {
Set<String> currentMembers = groupsConnection.getMembersOfGroup(groupKey); Set<String> currentMembers = groupsConnection.getMembersOfGroup(groupKey);
logger.atInfo().log("%s has %s members.", groupKey, currentMembers.size()); logger.atInfo().log("%s has %s members.", groupKey, currentMembers.size());
// One success is enough for validation.
return;
} catch (IOException e) { } catch (IOException e) {
logger.atWarning().withCause(e).log("Failed to check %s", groupKey); logger.atWarning().withCause(e).log("Failed to check %s", groupKey);
} }

View file

@ -93,13 +93,56 @@ public abstract class CredentialModule {
@AdcDelegatedCredential @AdcDelegatedCredential
@Provides @Provides
@Singleton @Singleton
public static GoogleCredentialsBundle provideSelfSignedDelegatedCredential( public static GoogleCredentialsBundle provideSelfSignedAdminDelegatedCredential(
@Config("defaultCredentialOauthScopes") ImmutableList<String> defaultScopes, @Config("defaultCredentialOauthScopes") ImmutableList<String> defaultScopes,
@Config("delegatedCredentialOauthScopes") ImmutableList<String> delegationScopes, @Config("delegatedCredentialOauthScopes") ImmutableList<String> delegationScopes,
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle, @ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("gSuiteAdminAccountEmailAddress") String gSuiteAdminAccountEmailAddress, @Config("gSuiteAdminAccountEmailAddress") String gSuiteAdminAccountEmailAddress,
@Config("tokenRefreshDelay") Duration tokenRefreshDelay, @Config("tokenRefreshDelay") Duration tokenRefreshDelay,
Clock clock) { Clock clock) {
return createSelfSignedDelegatedCredential(
defaultScopes,
delegationScopes,
credentialsBundle,
gSuiteAdminAccountEmailAddress,
tokenRefreshDelay,
clock);
}
/**
* Provides a {@link GoogleCredentialsBundle} for sending emails through Google Workspace.
*
* <p>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<String> defaultScopes,
@Config("delegatedCredentialOauthScopes") ImmutableList<String> 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<String> defaultScopes,
ImmutableList<String> delegationScopes,
GoogleCredentialsBundle credentialsBundle,
String gSuiteUserEmailAddress,
Duration tokenRefreshDelay,
Clock clock) {
GoogleCredentials signer = credentialsBundle.getGoogleCredentials(); GoogleCredentials signer = credentialsBundle.getGoogleCredentials();
checkArgument( checkArgument(
@ -118,7 +161,7 @@ public abstract class CredentialModule {
DelegatedCredentials.createSelfSignedDelegatedCredential( DelegatedCredentials.createSelfSignedDelegatedCredential(
(ServiceAccountSigner) signer, (ServiceAccountSigner) signer,
ImmutableList.<String>builder().addAll(defaultScopes).addAll(delegationScopes).build(), ImmutableList.<String>builder().addAll(defaultScopes).addAll(delegationScopes).build(),
gSuiteAdminAccountEmailAddress, gSuiteUserEmailAddress,
clock, clock,
tokenRefreshDelay); tokenRefreshDelay);
return GoogleCredentialsBundle.create(credential); return GoogleCredentialsBundle.create(credential);
@ -136,6 +179,15 @@ public abstract class CredentialModule {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface GoogleWorkspaceCredential {} 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 * Dagger qualifier for a credential with delegated admin access for a dasher domain (for Google
* Workspace) backed by the application default credential (ADC). * Workspace) backed by the application default credential (ADC).

View file

@ -538,6 +538,13 @@ public final class RegistryConfig {
return parseEmailAddress(config.gSuite.outgoingEmailAddress); 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. * The display name that is used on outgoing emails sent by Nomulus.
* *
@ -549,6 +556,16 @@ public final class RegistryConfig {
return config.gSuite.outgoingEmailDisplayName; 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. * 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); 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. * Returns the email address to which spec 11 email should be replied.
* *

View file

@ -77,6 +77,9 @@ public class RegistryConfigSettings {
public static class GSuite { public static class GSuite {
public String domainName; public String domainName;
public String outgoingEmailAddress; public String outgoingEmailAddress;
// TODO(b/279671974): remove below field after migration
public String newOutgoingEmailAddress;
public String replyToEmailAddress;
public String outgoingEmailDisplayName; public String outgoingEmailDisplayName;
public String adminAccountEmailAddress; public String adminAccountEmailAddress;
public String supportGroupEmailAddress; public String supportGroupEmailAddress;
@ -203,6 +206,8 @@ public class RegistryConfigSettings {
public static class Misc { public static class Misc {
public String sheetExportId; public String sheetExportId;
public String alertRecipientEmailAddress; public String alertRecipientEmailAddress;
// TODO(b/279671974): remove below field after migration
public String newAlertRecipientEmailAddress;
public String spec11OutgoingEmailAddress; public String spec11OutgoingEmailAddress;
public List<String> spec11BccEmailAddresses; public List<String> spec11BccEmailAddresses;
public int transientFailureRetries; public int transientFailureRetries;

View file

@ -33,6 +33,9 @@ gSuite:
# https://cloud.google.com/appengine/docs/standard/java/mail/#who_can_send_mail # https://cloud.google.com/appengine/docs/standard/java/mail/#who_can_send_mail
outgoingEmailDisplayName: Example Registry outgoingEmailDisplayName: Example Registry
outgoingEmailAddress: noreply@project-id.appspotmail.com 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 # Email address of the admin account on the G Suite app. This is used for
# logging in to perform administrative actions, not sending emails. # logging in to perform administrative actions, not sending emails.
@ -432,6 +435,9 @@ misc:
# Address we send alert summary emails to. # Address we send alert summary emails to.
alertRecipientEmailAddress: email@example.com 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 # 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. # to be a deliverable email address to handle replies from registrars as well.
spec11OutgoingEmailAddress: abuse@example.com spec11OutgoingEmailAddress: abuse@example.com

View file

@ -20,10 +20,12 @@ import com.google.api.services.gmail.Gmail;
import com.google.api.services.gmail.model.Message; import com.google.api.services.gmail.model.Message;
import com.google.common.net.MediaType; import com.google.common.net.MediaType;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.EmailMessage; import google.registry.util.EmailMessage;
import google.registry.util.EmailMessage.Attachment; import google.registry.util.EmailMessage.Attachment;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Properties; import java.util.Properties;
import javax.inject.Inject; import javax.inject.Inject;
import javax.mail.Address; import javax.mail.Address;
@ -42,10 +44,24 @@ import org.apache.commons.codec.binary.Base64;
public final class GmailClient { public final class GmailClient {
private final Gmail gmail; private final Gmail gmail;
private final InternetAddress outgoingEmailAddressWithUsername;
private final InternetAddress replyToEmailAddress;
@Inject @Inject
GmailClient(Gmail gmail) { GmailClient(
Gmail gmail,
@Config("gSuiteNewOutgoingEmailAddress") String gSuiteOutgoingEmailAddress,
@Config("gSuiteOutgoingEmailDisplayName") String gSuiteOutgoingEmailDisplayName,
@Config("replyToEmailAddress") InternetAddress replyToEmailAddress) {
this.gmail = gmail; 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) { public Message sendEmail(EmailMessage emailMessage) {
Message message = toGmailMessage(toMimeMessage(emailMessage)); Message message = toGmailMessage(toMimeMessage(emailMessage));
try { try {
// "me" is reserved word for the authorized user of the Gmail API.
return gmail.users().messages().send("me", message).execute(); return gmail.users().messages().send("me", message).execute();
} catch (IOException e) { } catch (IOException e) {
throw new EmailException(e); throw new EmailException(e);
@ -78,11 +95,12 @@ public final class GmailClient {
} }
} }
static MimeMessage toMimeMessage(EmailMessage emailMessage) { MimeMessage toMimeMessage(EmailMessage emailMessage) {
try { try {
MimeMessage msg = MimeMessage msg =
new MimeMessage(Session.getDefaultInstance(new Properties(), /* authenticator= */ null)); new MimeMessage(Session.getDefaultInstance(new Properties(), /* authenticator= */ null));
msg.setFrom(emailMessage.from()); msg.setFrom(this.outgoingEmailAddressWithUsername);
msg.setReplyTo(new InternetAddress[] {replyToEmailAddress});
msg.addRecipients( msg.addRecipients(
RecipientType.TO, toArray(emailMessage.recipients(), InternetAddress.class)); RecipientType.TO, toArray(emailMessage.recipients(), InternetAddress.class));
msg.setSubject(emailMessage.subject()); msg.setSubject(emailMessage.subject());

View file

@ -17,7 +17,7 @@ package google.registry.groups;
import com.google.api.services.gmail.Gmail; import com.google.api.services.gmail.Gmail;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import google.registry.config.CredentialModule.AdcDelegatedCredential; import google.registry.config.CredentialModule.GmailDelegatedCredential;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle; import google.registry.util.GoogleCredentialsBundle;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -29,7 +29,7 @@ public class GmailModule {
@Provides @Provides
@Singleton @Singleton
Gmail provideGmail( Gmail provideGmail(
@AdcDelegatedCredential GoogleCredentialsBundle credentialsBundle, @GmailDelegatedCredential GoogleCredentialsBundle credentialsBundle,
@Config("projectId") String projectId) { @Config("projectId") String projectId) {
return new Gmail.Builder( return new Gmail.Builder(
credentialsBundle.getHttpTransport(), credentialsBundle.getHttpTransport(),

View file

@ -46,6 +46,7 @@ public abstract class EmailMessage {
public abstract ImmutableSet<InternetAddress> recipients(); public abstract ImmutableSet<InternetAddress> recipients();
// TODO(b/279671974): remove `from` after migration.
public abstract InternetAddress from(); public abstract InternetAddress from();
public abstract ImmutableSet<InternetAddress> ccs(); public abstract ImmutableSet<InternetAddress> ccs();