1
0
mirror of https://github.com/google/nomulus synced 2026-05-16 12:51:47 +00:00

Verify user can send email (#3045)

Change the CannedScriptExecutionAction to send a email
message as a user-specified G workspace user.

This change is part of b/510340944, to verify that a newly
added dedicated sender is properly set up for sending emails.

Once the new sender is tested, the changes in this PR can be
dropped.
This commit is contained in:
Weimin Yu
2026-05-14 11:13:57 -04:00
committed by GitHub
parent 56fe588b56
commit d9d83205c7
4 changed files with 177 additions and 34 deletions

View File

@@ -628,6 +628,18 @@
}
}
},
"node_modules/@angular/build/node_modules/@types/node": {
"version": "25.7.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@types/node/-/node-25.7.0.tgz",
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.21.0"
}
},
"node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz",
@@ -641,6 +653,24 @@
"vite": "^6.0.0 || ^7.0.0"
}
},
"node_modules/@angular/build/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/build/node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -664,6 +694,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular/build/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/build/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -697,6 +743,15 @@
"node": ">= 12"
}
},
"node_modules/@angular/build/node_modules/undici-types": {
"version": "7.21.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/undici-types/-/undici-types-7.21.0.tgz",
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@angular/build/node_modules/vite": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
@@ -916,6 +971,24 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@angular/cli/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/cli/node_modules/cli-spinners": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
@@ -1019,6 +1092,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular/cli/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/cli/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -4606,6 +4695,24 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@schematics/angular/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@schematics/angular/node_modules/cli-spinners": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
@@ -4709,6 +4816,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@schematics/angular/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@schematics/angular/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",

View File

@@ -54,9 +54,15 @@ public class BatchModule {
static final int DEFAULT_MAX_QPS = 10;
@Provides
@Parameter("url")
static String provideUrl(HttpServletRequest req) {
return extractRequiredParameter(req, "url");
@Parameter("sender")
static String provideSender(HttpServletRequest req) {
return extractRequiredParameter(req, "sender");
}
@Provides
@Parameter("receiver")
static String provideReceiver(HttpServletRequest req) {
return extractRequiredParameter(req, "receiver");
}
@Provides

View File

@@ -16,30 +16,32 @@ package google.registry.batch;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.services.gmail.Gmail;
import com.google.common.flogger.FluentLogger;
import dagger.Lazy;
import google.registry.config.RegistryConfig.Config;
import google.registry.groups.GmailClient;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.UrlConnectionService;
import google.registry.request.UrlConnectionUtils;
import google.registry.request.auth.Auth;
import google.registry.util.EmailMessage;
import google.registry.util.Retrier;
import jakarta.inject.Inject;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
/**
* Action that executes a canned script specified by the caller.
*
* <p>This class provides a hook for invoking hard-coded methods. The main use case is to verify in
* Sandbox and Production environments new features that depend on environment-specific
* configurations. For example, the {@code DelegatedCredential}, which requires correct GWorkspace
* configuration, has been tested this way. Since it is a hassle to add or remove endpoints, we keep
* this class all the time.
* configurations.
*
* <p>This action can be invoked using the Nomulus CLI command: {@code nomulus -e ${env} curl
* --service BACKEND -X POST -u '/_dr/task/executeCannedScript}'}
* --service BACKEND -X POST -d 'sender=sender@example.com' -d 'receiver=receiver@example.com' -u
* '/_dr/task/executeCannedScript'}
*/
@Action(
service = Action.Service.BACKEND,
@@ -50,39 +52,50 @@ import javax.net.ssl.HttpsURLConnection;
public class CannedScriptExecutionAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject UrlConnectionService urlConnectionService;
@Inject Lazy<Gmail> gmail;
@Inject Retrier retrier;
@Inject
@Config("isEmailSendingEnabled")
boolean isEmailSendingEnabled;
@Inject Response response;
@Inject
@Parameter("url")
String url;
@Parameter("sender")
String sender;
@Inject
@Parameter("receiver")
String receiver;
@Inject
CannedScriptExecutionAction() {}
@Override
public void run() {
Integer responseCode = null;
String responseContent = null;
// For b/510340944, validating a new G Workspace user can send email. Code below can be
// removed or changed afterward.
try {
logger.atInfo().log("Connecting to: %s", url);
HttpsURLConnection connection =
(HttpsURLConnection) urlConnectionService.createConnection(new URL(url));
responseCode = connection.getResponseCode();
logger.atInfo().log("Code: %d", responseCode);
logger.atInfo().log("Headers: %s", connection.getHeaderFields());
responseContent = new String(UrlConnectionUtils.getResponseBytes(connection), UTF_8);
logger.atInfo().log("Response: %s", responseContent);
logger.atInfo().log("Sending email from %s to %s", sender, receiver);
GmailClient gmailClient =
new GmailClient(
gmail, retrier, isEmailSendingEnabled, sender, sender, new InternetAddress(sender));
gmailClient.sendEmail(
EmailMessage.newBuilder()
.addRecipient(new InternetAddress(receiver))
.setSubject(String.format("Email send test from %s", sender))
.setBody(String.format("This is a test email sent from %s to %s.", sender, receiver))
.build());
response.setPayload("Email sent successfully.");
} catch (AddressException e) {
logger.atWarning().withCause(e).log(
"Invalid email address: sender=%s, receiver=%s", sender, receiver);
response.setStatus(400);
response.setPayload("Invalid email address provided.");
} catch (Exception e) {
logger.atWarning().withCause(e).log("Connection to %s failed", url);
logger.atSevere().withCause(e).log("Failed to send email");
throw new RuntimeException(e);
} finally {
if (responseCode != null) {
response.setStatus(responseCode);
}
if (responseContent != null) {
response.setPayload(responseContent);
}
}
}
}

View File

@@ -56,8 +56,9 @@ public final class GmailClient {
private final InternetAddress outgoingEmailAddressWithUsername;
private final InternetAddress replyToEmailAddress;
// TODO(b/510340944): make package private after feature is rolled out
@Inject
GmailClient(
public GmailClient(
Lazy<Gmail> gmail,
Retrier retrier,
@Config("isEmailSendingEnabled") boolean isEmailSendingEnabled,