From fe6bc628aa3ab6413442a7c0ab4fd67d3b3cb1f7 Mon Sep 17 00:00:00 2001 From: Weimin Yu Date: Fri, 9 Jun 2023 13:06:21 -0400 Subject: [PATCH] Add Gmail Client and set up tests (#2048) * Add Gmail Client and set up tests Add a Gmail client and manually triggered email tests in CannedScriptExecutionActon. We will test Gmail with Google Workspace in Sandbox, since Alpha and Crash are not properly set up for Google Workspace, and we have not figured out why. --- .../batch/CannedScriptExecutionAction.java | 79 +++++++++++- .../registry/config/files/default-config.yaml | 2 + .../google/registry/groups/GmailClient.java | 120 ++++++++++++++++++ .../google/registry/groups/GmailModule.java | 41 ++++++ .../module/backend/BackendComponent.java | 2 + 5 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/google/registry/groups/GmailClient.java create mode 100644 core/src/main/java/google/registry/groups/GmailModule.java diff --git a/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java b/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java index aa732d72d..c1fceb2e5 100644 --- a/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java +++ b/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java @@ -14,12 +14,26 @@ package google.registry.batch; +import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.request.Action.Method.POST; +import static google.registry.util.RegistrarUtils.normalizeRegistrarId; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Streams; import com.google.common.flogger.FluentLogger; +import google.registry.config.RegistryConfig.Config; +import google.registry.groups.GmailClient; +import google.registry.groups.GroupsConnection; +import google.registry.model.registrar.Registrar; +import google.registry.model.registrar.RegistrarPoc; import google.registry.request.Action; import google.registry.request.auth.Auth; +import google.registry.util.EmailMessage; +import java.io.IOException; +import java.util.Set; import javax.inject.Inject; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; /** * Action that executes a canned script specified by the caller. @@ -42,19 +56,80 @@ import javax.inject.Inject; public class CannedScriptExecutionAction implements Runnable { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final GroupsConnection groupsConnection; + private final GmailClient gmailClient; + + private final InternetAddress senderAddress; + + private final InternetAddress recipientAddress; + + private final String gSuiteDomainName; + @Inject - CannedScriptExecutionAction() { - logger.atInfo().log("Received request to run scripts."); + CannedScriptExecutionAction( + GroupsConnection groupsConnection, + GmailClient gmailClient, + @Config("projectId") String projectId, + @Config("gSuiteDomainName") String gSuiteDomainName, + @Config("alertRecipientEmailAddress") InternetAddress recipientAddress) { + this.groupsConnection = groupsConnection; + this.gmailClient = gmailClient; + this.gSuiteDomainName = gSuiteDomainName; + try { + this.senderAddress = new InternetAddress(String.format("%s@%s", projectId, gSuiteDomainName)); + } catch (AddressException e) { + throw new RuntimeException(e); + } + this.recipientAddress = recipientAddress; + logger.atInfo().log("Sender:%s; Recipient: %s.", this.senderAddress, this.recipientAddress); } @Override public void run() { try { // Invoke canned scripts here. + checkGroupApi(); + EmailMessage message = createEmail(); + this.gmailClient.sendEmail(message); logger.atInfo().log("Finished running scripts."); } catch (Throwable t) { logger.atWarning().withCause(t).log("Error executing scripts."); throw new RuntimeException("Execution failed."); } } + + // Checks if Directory and GroupSettings still work after GWorkspace changes. + void checkGroupApi() { + ImmutableList registrars = + Streams.stream(Registrar.loadAllCached()) + .filter(registrar -> registrar.isLive() && registrar.getType() == Registrar.Type.REAL) + .collect(toImmutableList()); + logger.atInfo().log("Found %s registrars.", registrars.size()); + for (Registrar registrar : registrars) { + for (final RegistrarPoc.Type type : RegistrarPoc.Type.values()) { + String groupKey = + String.format( + "%s-%s-contacts@%s", + normalizeRegistrarId(registrar.getRegistrarId()), + type.getDisplayName(), + gSuiteDomainName); + try { + Set currentMembers = groupsConnection.getMembersOfGroup(groupKey); + logger.atInfo().log("%s has %s members.", groupKey, currentMembers.size()); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Failed to check %s", groupKey); + } + } + } + logger.atInfo().log("Finished checking GroupApis."); + } + + EmailMessage createEmail() { + return EmailMessage.newBuilder() + .setFrom(senderAddress) + .setSubject("Test: Please ignore.") + .setRecipients(ImmutableList.of(recipientAddress)) + .setBody("Sent from Nomulus through Google Workspace.") + .build(); + } } 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 ae6b38955..f41f4fe30 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 @@ -334,6 +334,8 @@ credentialOAuth: - https://www.googleapis.com/auth/admin.directory.group # View and manage group settings in Group Settings API. - https://www.googleapis.com/auth/apps.groups.settings + # Send email through Gmail. + - https://www.googleapis.com/auth/gmail.send # OAuth scopes required to create a credential locally in for the nomulus tool. localCredentialOauthScopes: # View and manage data in all Google Cloud APIs. diff --git a/core/src/main/java/google/registry/groups/GmailClient.java b/core/src/main/java/google/registry/groups/GmailClient.java new file mode 100644 index 000000000..aca725bfe --- /dev/null +++ b/core/src/main/java/google/registry/groups/GmailClient.java @@ -0,0 +1,120 @@ +// Copyright 2023 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.groups; + +import static com.google.common.collect.Iterables.toArray; + +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.util.EmailMessage; +import google.registry.util.EmailMessage.Attachment; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Properties; +import javax.inject.Inject; +import javax.mail.Address; +import javax.mail.BodyPart; +import javax.mail.Message.RecipientType; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import org.apache.commons.codec.binary.Base64; + +/** Sends {@link EmailMessage EmailMessages} through Google Workspace using {@link Gmail}. */ +public final class GmailClient { + + private final Gmail gmail; + + @Inject + GmailClient(Gmail gmail) { + this.gmail = gmail; + } + + /** + * Sends {@code emailMessage} using {@link Gmail}. + * + *

If the sender as specified by {@link EmailMessage#from} differs from the caller's identity, + * the caller must have delegated `send` authority to the sender. + */ + @CanIgnoreReturnValue + public Message sendEmail(EmailMessage emailMessage) { + Message message = toGmailMessage(toMimeMessage(emailMessage)); + try { + return gmail.users().messages().send("me", message).execute(); + } catch (IOException e) { + throw new EmailException(e); + } + } + + static Message toGmailMessage(MimeMessage message) { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + message.writeTo(buffer); + byte[] rawMessageBytes = buffer.toByteArray(); + String encodedEmail = Base64.encodeBase64URLSafeString(rawMessageBytes); + Message gmailMessage = new Message(); + gmailMessage.setRaw(encodedEmail); + return gmailMessage; + } catch (MessagingException | IOException e) { + throw new EmailException(e); + } + } + + static MimeMessage toMimeMessage(EmailMessage emailMessage) { + try { + MimeMessage msg = + new MimeMessage(Session.getDefaultInstance(new Properties(), /* authenticator= */ null)); + msg.setFrom(emailMessage.from()); + msg.addRecipients( + RecipientType.TO, toArray(emailMessage.recipients(), InternetAddress.class)); + msg.setSubject(emailMessage.subject()); + + Multipart multipart = new MimeMultipart(); + BodyPart bodyPart = new MimeBodyPart(); + bodyPart.setContent( + emailMessage.body(), + emailMessage.contentType().orElse(MediaType.PLAIN_TEXT_UTF_8).toString()); + multipart.addBodyPart(bodyPart); + + if (emailMessage.attachment().isPresent()) { + Attachment attachment = emailMessage.attachment().get(); + BodyPart attachmentPart = new MimeBodyPart(); + attachmentPart.setContent(attachment.content(), attachment.contentType().toString()); + attachmentPart.setFileName(attachment.filename()); + multipart.addBodyPart(attachmentPart); + } + msg.addRecipients(RecipientType.BCC, toArray(emailMessage.bccs(), Address.class)); + msg.addRecipients(RecipientType.CC, toArray(emailMessage.ccs(), Address.class)); + msg.setContent(multipart); + msg.saveChanges(); + return msg; + } catch (MessagingException e) { + throw new EmailException(e); + } + } + + static class EmailException extends RuntimeException { + + public EmailException(Throwable cause) { + super(cause); + } + } +} diff --git a/core/src/main/java/google/registry/groups/GmailModule.java b/core/src/main/java/google/registry/groups/GmailModule.java new file mode 100644 index 000000000..28f4a3f8e --- /dev/null +++ b/core/src/main/java/google/registry/groups/GmailModule.java @@ -0,0 +1,41 @@ +// Copyright 2023 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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.RegistryConfig.Config; +import google.registry.util.GoogleCredentialsBundle; +import javax.inject.Singleton; + +/** Dagger module providing {@link Gmail} API. */ +@Module +public class GmailModule { + + @Provides + @Singleton + Gmail provideGmail( + @AdcDelegatedCredential GoogleCredentialsBundle credentialsBundle, + @Config("projectId") String projectId) { + return new Gmail.Builder( + credentialsBundle.getHttpTransport(), + credentialsBundle.getJsonFactory(), + credentialsBundle.getHttpRequestInitializer()) + .setApplicationName(projectId) + .build(); + } +} diff --git a/core/src/main/java/google/registry/module/backend/BackendComponent.java b/core/src/main/java/google/registry/module/backend/BackendComponent.java index dc7693616..b17493243 100644 --- a/core/src/main/java/google/registry/module/backend/BackendComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendComponent.java @@ -28,6 +28,7 @@ import google.registry.export.sheet.SheetsServiceModule; import google.registry.flows.ServerTridProviderModule; import google.registry.flows.custom.CustomLogicFactoryModule; import google.registry.groups.DirectoryModule; +import google.registry.groups.GmailModule; import google.registry.groups.GroupsModule; import google.registry.groups.GroupssettingsModule; import google.registry.keyring.KeyringModule; @@ -64,6 +65,7 @@ import javax.inject.Singleton; DirectoryModule.class, DummyKeyringModule.class, DriveModule.class, + GmailModule.class, GroupsModule.class, GroupssettingsModule.class, JSchModule.class,