diff --git a/console-webapp/package-lock.json b/console-webapp/package-lock.json index 5b0bec4b47b..afdbc88ff9a 100644 --- a/console-webapp/package-lock.json +++ b/console-webapp/package-lock.json @@ -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", diff --git a/core/src/main/java/google/registry/batch/BatchModule.java b/core/src/main/java/google/registry/batch/BatchModule.java index e4b9c6a4842..b2854c2fabf 100644 --- a/core/src/main/java/google/registry/batch/BatchModule.java +++ b/core/src/main/java/google/registry/batch/BatchModule.java @@ -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 diff --git a/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java b/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java index ca68a2f7fb3..a83e4899796 100644 --- a/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java +++ b/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java @@ -16,30 +16,32 @@ 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. * *

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. * *

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 @@ public class CannedScriptExecutionAction implements Runnable { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - @Inject UrlConnectionService urlConnectionService; + @Inject Lazy 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); - } } } } diff --git a/core/src/main/java/google/registry/groups/GmailClient.java b/core/src/main/java/google/registry/groups/GmailClient.java index fb223faaa46..32245b0f035 100644 --- a/core/src/main/java/google/registry/groups/GmailClient.java +++ b/core/src/main/java/google/registry/groups/GmailClient.java @@ -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, Retrier retrier, @Config("isEmailSendingEnabled") boolean isEmailSendingEnabled,