diff options
author | Bjørn Christian Seime <bjorncs@vespa.ai> | 2023-10-20 15:49:28 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@vespa.ai> | 2023-10-20 16:18:28 +0200 |
commit | 12b1d52427cc3decb98f24f24739affe4fc3fb09 (patch) | |
tree | e10abd5811d3514a3427646aa5e7fc1bc309cb3f /controller-server/src/main/java | |
parent | 15348a70192e827ee8c1df14ad22493bcae90570 (diff) |
Move out mail templating to separate class
Diffstat (limited to 'controller-server/src/main/java')
7 files changed, 143 insertions, 87 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java index 87885bc5f21..bab86e7bfde 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java @@ -134,7 +134,7 @@ public class Controller extends AbstractComponent { notifier = new Notifier(curator, serviceRegistry.zoneRegistry(), serviceRegistry.mailer(), flagSource); notificationsDb = new NotificationsDb(this); supportAccessControl = new SupportAccessControl(this); - mailVerifier = new MailVerifier(serviceRegistry.zoneRegistry().dashboardUrl(), tenantController, serviceRegistry.mailer(), curator, clock); + mailVerifier = new MailVerifier(serviceRegistry.zoneRegistry(), tenantController, serviceRegistry.mailer(), curator, clock); dataplaneTokenService = new DataplaneTokenService(this); // Record the version of this controller diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java index ecdfc5990c0..7d8f0d260fc 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java @@ -6,22 +6,21 @@ import com.yahoo.vespa.hosted.controller.LockedTenant; import com.yahoo.vespa.hosted.controller.TenantController; import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail; import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; +import com.yahoo.vespa.hosted.controller.notification.MailTemplating; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; +import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.TenantContacts; import com.yahoo.vespa.hosted.controller.tenant.TenantInfo; -import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification; -import java.net.URI; import java.time.Clock; import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.UUID; -import static com.yahoo.yolean.Exceptions.uncheck; - /** * @author olaa @@ -34,14 +33,14 @@ public class MailVerifier { private final Mailer mailer; private final CuratorDb curatorDb; private final Clock clock; - private final URI dashboardUri; + private final MailTemplating mailTemplating; - public MailVerifier(URI dashboardUri, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) { + public MailVerifier(ZoneRegistry zoneRegistry, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) { this.tenantController = tenantController; this.mailer = mailer; this.curatorDb = curatorDb; this.clock = clock; - this.dashboardUri = dashboardUri; + this.mailTemplating = new MailTemplating(zoneRegistry); } public PendingMailVerification sendMailVerification(TenantName tenantName, String email, PendingMailVerification.MailType mailType) { @@ -133,12 +132,7 @@ public class MailVerifier { } private Mail mailOf(PendingMailVerification pendingMailVerification) { - var classLoader = this.getClass().getClassLoader(); - var template = uncheck(() -> classLoader.getResourceAsStream("mail/mail-verification.tmpl").readAllBytes()); - var message = new String(template) - .replaceAll("%\\{consoleUrl}", dashboardUri.getHost()) - .replaceAll("%\\{email}", pendingMailVerification.getMailAddress()) - .replaceAll("%\\{code}", pendingMailVerification.getVerificationCode()); + var message = mailTemplating.generateMailVerificationHtml(pendingMailVerification); return new Mail(List.of(pendingMailVerification.getMailAddress()), "Please verify your email", "", message); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java index ba2e3099d5d..d0426416349 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.flags.ListFlag; import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; +import com.yahoo.vespa.hosted.controller.notification.MailTemplating; import com.yahoo.vespa.hosted.controller.notification.Notification; import com.yahoo.vespa.hosted.controller.notification.NotificationSource; import com.yahoo.vespa.hosted.controller.persistence.TrialNotifications; @@ -160,7 +161,7 @@ public class CloudTrialExpirer extends ControllerMaintainer { } private void queueNotification(Tenant tenant, String consoleMsg, String emailSubject, String emailMsg) { - var mail = Optional.of(Notification.MailContent.fromTemplate("default-mail-content") + var mail = Optional.of(Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT) .subject(emailSubject) .with("mailMessageTemplate", "cloud-trial-notification") .with("cloudTrialMessage", emailMsg) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java new file mode 100644 index 00000000000..e8fb7289f4c --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java @@ -0,0 +1,117 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.vespa.hosted.controller.notification; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.restapi.UriBuilder; +import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; +import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification; +import com.yahoo.yolean.Exceptions; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.runtime.resource.loader.StringResourceLoader; +import org.apache.velocity.runtime.resource.util.StringResourceRepository; +import org.apache.velocity.tools.generic.EscapeTool; + +import java.io.StringWriter; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +/** + * @author bjorncs + */ +public class MailTemplating { + + public enum Template { + MAIL("mail"), DEFAULT_MAIL_CONTENT("default-mail-content"), NOTIFICATION_MESSAGE("notification-message"), + CLOUD_TRIAL_NOTIFICATION("cloud-trial-notification"), MAIL_VERIFICATION("mail-verification"); + + public static Optional<Template> fromId(String id) { + return Arrays.stream(values()).filter(t -> t.id.equals(id)).findAny(); + } + + private final String id; + + Template(String id) { this.id = id; } + + public String getId() { return id; } + } + + private final VelocityEngine velocity; + private final EscapeTool escapeTool = new EscapeTool(); + private final URI dashboardUri; + + public MailTemplating(ZoneRegistry zoneRegistry) { + this.velocity = createTemplateEngine(); + this.dashboardUri = zoneRegistry.dashboardUrl(); + } + + public String generateDefaultMailHtml(Template mailBodyTemplate, Map<String, Object> params, TenantName tenant) { + var ctx = createVelocityContext(); + ctx.put("accountNotificationLink", accountNotificationsUri(tenant)); + ctx.put("privacyPolicyLink", "https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"); + ctx.put("termsOfServiceLink", consoleUri("terms-of-service-trial.html")); + ctx.put("supportLink", consoleUri("support")); + ctx.put("mailBodyTemplate", mailBodyTemplate.getId()); + params.forEach(ctx::put); + return render(ctx, Template.MAIL); + } + + public String generateMailVerificationHtml(PendingMailVerification pmf) { + var ctx = createVelocityContext(); + ctx.put("consoleLink", dashboardUri.getHost()); + ctx.put("email", pmf.getMailAddress()); + ctx.put("code", pmf.getVerificationCode()); + return render(ctx, Template.MAIL_VERIFICATION); + } + + public String escapeHtml(String s) { return escapeTool.html(s); } + + private VelocityContext createVelocityContext() { + var ctx = new VelocityContext(); + ctx.put("esc", escapeTool); + return ctx; + } + + private String render(VelocityContext ctx, Template template) { + var writer = new StringWriter(); + // Ignoring return value - implementation either returns 'true' or throws, never 'false' + velocity.mergeTemplate(template.getId(), StandardCharsets.UTF_8.name(), ctx, writer); + return writer.toString(); + } + + private static VelocityEngine createTemplateEngine() { + var v = new VelocityEngine(); + v.setProperty(Velocity.RESOURCE_LOADERS, "string"); + v.setProperty(Velocity.RESOURCE_LOADER + ".string.class", StringResourceLoader.class.getName()); + v.setProperty(Velocity.RESOURCE_LOADER + ".string.repository.static", "false"); + v.init(); + var repo = (StringResourceRepository) v.getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT); + Arrays.stream(Template.values()).forEach(t -> registerTemplate(repo, t.getId())); + return v; + } + + private static void registerTemplate(StringResourceRepository repo, String name) { + var templateStr = Exceptions.uncheck(() -> { + var in = MailTemplating.class.getResourceAsStream("/mail/%s.vm".formatted(name)); + return new String(in.readAllBytes()); + }); + repo.putStringResource(name, templateStr); + } + + private String accountNotificationsUri(TenantName tenant) { + return new UriBuilder(dashboardUri) + .append("tenant/") + .append(tenant.value()) + .append("account/notifications") + .toString(); + } + + private String consoleUri(String path) { + return new UriBuilder(dashboardUri).append(path).toString(); + } +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java index 525a768457f..4a94098ce98 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java @@ -90,7 +90,7 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve } public static class MailContent { - private final String template; + private final MailTemplating.Template template; private final SortedMap<String, Object> values; private final String subject; @@ -100,18 +100,18 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve subject = b.subject; } - public String template() { return template; } + public MailTemplating.Template template() { return template; } public SortedMap<String, Object> values() { return Collections.unmodifiableSortedMap(values); } public Optional<String> subject() { return Optional.ofNullable(subject); } - public static Builder fromTemplate(String template) { return new Builder(template); } + public static Builder fromTemplate(MailTemplating.Template template) { return new Builder(template); } public static class Builder { - private final String template; + private final MailTemplating.Template template; private final Map<String, Object> values = new HashMap<>(); private String subject; - private Builder(String template) { + private Builder(MailTemplating.Template template) { this.template = template; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java index 6468a4c397b..e3bfb8b4c56 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.notification; import com.google.common.annotations.VisibleForTesting; import com.yahoo.config.provision.Environment; -import com.yahoo.config.provision.TenantName; import com.yahoo.restapi.UriBuilder; import com.yahoo.text.Text; import com.yahoo.vespa.flags.FetchVector; @@ -16,17 +15,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.TenantContacts; -import com.yahoo.yolean.Exceptions; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.Velocity; -import org.apache.velocity.app.VelocityEngine; -import org.apache.velocity.runtime.resource.loader.StringResourceLoader; -import org.apache.velocity.runtime.resource.util.StringResourceRepository; -import org.apache.velocity.tools.generic.EscapeTool; - -import java.io.StringWriter; + import java.net.URI; -import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -46,7 +36,7 @@ public class Notifier { private final FlagSource flagSource; private final NotificationFormatter formatter; private final URI dashboardUri; - private final VelocityEngine velocity; + private final MailTemplating mailTemplating; private static final Logger log = Logger.getLogger(Notifier.class.getName()); @@ -59,29 +49,7 @@ public class Notifier { this.flagSource = Objects.requireNonNull(flagSource); this.formatter = new NotificationFormatter(zoneRegistry); this.dashboardUri = zoneRegistry.dashboardUrl(); - this.velocity = createTemplateEngine(); - } - - private static VelocityEngine createTemplateEngine() { - var v = new VelocityEngine(); - v.setProperty(Velocity.RESOURCE_LOADERS, "string"); - v.setProperty(Velocity.RESOURCE_LOADER + ".string.class", StringResourceLoader.class.getName()); - v.setProperty(Velocity.RESOURCE_LOADER + ".string.repository.static", "false"); - v.init(); - var repo = (StringResourceRepository) v.getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT); - registerTemplate(repo, "mail"); - registerTemplate(repo, "default-mail-content"); - registerTemplate(repo, "notification-message"); - registerTemplate(repo, "cloud-trial-notification"); - return v; - } - - private static void registerTemplate(StringResourceRepository repo, String name) { - var templateStr = Exceptions.uncheck(() -> { - var in = Notifier.class.getResourceAsStream("/mail/%s.vm".formatted(name)); - return new String(in.readAllBytes()); - }); - repo.putStringResource(name, templateStr); + this.mailTemplating = new MailTemplating(zoneRegistry); } public void dispatch(List<Notification> notifications, NotificationSource source) { @@ -154,26 +122,13 @@ public class Notifier { } private String generateHtml(FormattedNotification content) { - var esc = new EscapeTool(); - var mailContent = content.notification().mailContent().orElseGet(() -> generateContentFromMessages(content, esc)); - var ctx = new VelocityContext(); - ctx.put("esc", esc); - ctx.put("accountNotificationLink", accountNotificationsUri(content.notification().source().tenant())); - ctx.put("privacyPolicyLink", "https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"); - ctx.put("termsOfServiceLink", consoleUri("terms-of-service-trial.html")); - ctx.put("supportLink", consoleUri("support")); - ctx.put("mailBodyTemplate", mailContent.template()); - mailContent.values().forEach(ctx::put); - - var writer = new StringWriter(); - // Ignoring return value - implementation either returns 'true' or throws, never 'false' - velocity.mergeTemplate("mail", StandardCharsets.UTF_8.name(), ctx, writer); - return writer.toString(); + var mailContent = content.notification().mailContent().orElseGet(() -> generateContentFromMessages(content)); + return mailTemplating.generateDefaultMailHtml(mailContent.template(), mailContent.values(), content.notification().source().tenant()); } - private Notification.MailContent generateContentFromMessages(FormattedNotification f, EscapeTool esc) { - var items = f.notification().messages().stream().map(m -> capitalise(linkify(esc.html(m)))).toList(); - return Notification.MailContent.fromTemplate("default-mail-content") + private Notification.MailContent generateContentFromMessages(FormattedNotification f) { + var items = f.notification().messages().stream().map(m -> capitalise(linkify(mailTemplating.escapeHtml(m)))).toList(); + return Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT) .with("mailMessageTemplate", "notification-message") .with("mailTitle", "Vespa Cloud Notifications") .with("notificationHeader", f.messagePrefix()) @@ -195,18 +150,6 @@ public class Notifier { return sb.toString(); } - private String accountNotificationsUri(TenantName tenant) { - return new UriBuilder(dashboardUri) - .append("tenant/") - .append(tenant.value()) - .append("account/notifications") - .toString(); - } - - private String consoleUri(String path) { - return new UriBuilder(dashboardUri).append(path).toString(); - } - private String notificationLink(NotificationSource source) { var uri = new UriBuilder(dashboardUri); uri = uri.append("tenant").append(source.tenant().value()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java index 52766f699ac..d5be4d22dc2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java @@ -12,6 +12,7 @@ import com.yahoo.slime.ObjectTraverser; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.notification.MailTemplating; import com.yahoo.vespa.hosted.controller.notification.Notification; import com.yahoo.vespa.hosted.controller.notification.NotificationSource; @@ -66,7 +67,7 @@ public class NotificationsSerializer { notification.source().runNumber().ifPresent(runNumber -> notificationObject.setLong(runNumberField, runNumber)); notification.mailContent().ifPresent(mc -> { - notificationObject.setString("mail-template", mc.template()); + notificationObject.setString("mail-template", mc.template().getId()); mc.subject().ifPresent(s -> notificationObject.setString("mail-subject", s)); var mailParamsCursor = notificationObject.setObject("mail-params"); mc.values().forEach((key, value) -> { @@ -119,7 +120,7 @@ public class NotificationsSerializer { private Optional<Notification.MailContent> mailContentFrom(final Inspector inspector) { return SlimeUtils.optionalString(inspector.field("mail-template")).map(template -> { - var builder = Notification.MailContent.fromTemplate(template); + var builder = Notification.MailContent.fromTemplate(MailTemplating.Template.fromId(template).orElseThrow()); SlimeUtils.optionalString(inspector.field("mail-subject")).ifPresent(builder::subject); inspector.field("mail-params").traverse((ObjectTraverser) (name, insp) -> { switch (insp.type()) { |